From 50f4f20024ad7f3d7a89791416aabb9a7cc2974c Mon Sep 17 00:00:00 2001 From: Clement Denis Date: Tue, 8 Oct 2024 19:52:12 +0100 Subject: [PATCH] add missing tests --- test-images/test-dom/Dockerfile | 6 + test-images/test-dom/README.md | 33 ++ test-images/test-dom/call-it_test.js | 19 + test-images/test-dom/class-it_test.js | 36 ++ test-images/test-dom/colorful-arms_test.js | 72 ++++ test-images/test-dom/colorful-legs_test.js | 56 +++ test-images/test-dom/embedded-organs_test.js | 90 +++++ test-images/test-dom/entrypoint.sh | 6 + test-images/test-dom/first-hello_test.js | 56 +++ test-images/test-dom/first-move_test.js | 40 ++ test-images/test-dom/first-wink_test.js | 45 +++ test-images/test-dom/package.json | 3 + .../test-dom/select-then-style_test.js | 44 +++ test-images/test-dom/test.js | 174 +++++++++ test-images/test-dom/the-skeleton_test.js | 44 +++ test-images/test-dom/yarn.lock | 315 ++++++++++++++++ test-images/test-js/Dockerfile | 22 ++ test-images/test-js/README.md | 31 ++ test-images/test-js/entrypoint.sh | 10 + test-images/test-js/test.mjs | 347 ++++++++++++++++++ .../test-js/tests/declare-everything.json | 14 + test-images/test-js/tests/first-function.json | 22 ++ .../test-js/tests/glance-on-power.json | 18 + test-images/test-js/tests/good-recipe.json | 50 +++ .../test-js/tests/i-win-arguments.json | 66 ++++ test-images/test-js/tests/listed.json | 58 +++ test-images/test-js/tests/objects-around.json | 26 ++ test-images/test-js/tests/only-if.json | 82 +++++ .../test-js/tests/play-with-variables.json | 22 ++ test-images/test-js/tests/star-forge.json | 6 + .../test-js/tests/the-smooth-operator.json | 26 ++ .../test-js/tests/transform-objects.json | 22 ++ 32 files changed, 1861 insertions(+) create mode 100644 test-images/test-dom/Dockerfile create mode 100644 test-images/test-dom/README.md create mode 100644 test-images/test-dom/call-it_test.js create mode 100644 test-images/test-dom/class-it_test.js create mode 100644 test-images/test-dom/colorful-arms_test.js create mode 100644 test-images/test-dom/colorful-legs_test.js create mode 100644 test-images/test-dom/embedded-organs_test.js create mode 100755 test-images/test-dom/entrypoint.sh create mode 100644 test-images/test-dom/first-hello_test.js create mode 100644 test-images/test-dom/first-move_test.js create mode 100644 test-images/test-dom/first-wink_test.js create mode 100644 test-images/test-dom/package.json create mode 100644 test-images/test-dom/select-then-style_test.js create mode 100644 test-images/test-dom/test.js create mode 100644 test-images/test-dom/the-skeleton_test.js create mode 100644 test-images/test-dom/yarn.lock create mode 100644 test-images/test-js/Dockerfile create mode 100644 test-images/test-js/README.md create mode 100755 test-images/test-js/entrypoint.sh create mode 100644 test-images/test-js/test.mjs create mode 100644 test-images/test-js/tests/declare-everything.json create mode 100644 test-images/test-js/tests/first-function.json create mode 100644 test-images/test-js/tests/glance-on-power.json create mode 100644 test-images/test-js/tests/good-recipe.json create mode 100644 test-images/test-js/tests/i-win-arguments.json create mode 100644 test-images/test-js/tests/listed.json create mode 100644 test-images/test-js/tests/objects-around.json create mode 100644 test-images/test-js/tests/only-if.json create mode 100644 test-images/test-js/tests/play-with-variables.json create mode 100644 test-images/test-js/tests/star-forge.json create mode 100644 test-images/test-js/tests/the-smooth-operator.json create mode 100644 test-images/test-js/tests/transform-objects.json diff --git a/test-images/test-dom/Dockerfile b/test-images/test-dom/Dockerfile new file mode 100644 index 0000000..bd06f12 --- /dev/null +++ b/test-images/test-dom/Dockerfile @@ -0,0 +1,6 @@ +FROM buildkite/puppeteer:7.1.0 + +WORKDIR /app +COPY dom . +COPY subjects ./subjects +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/test-images/test-dom/README.md b/test-images/test-dom/README.md new file mode 100644 index 0000000..027408d --- /dev/null +++ b/test-images/test-dom/README.md @@ -0,0 +1,33 @@ +# DOM + +Tests that use puppeteer to do browser side exercises + +## Run test locally + +### Installation + +> You need node version 14+ + +```bash +# Clone the repo +git clone https://github.com/01-edu/public.git + +# go into the dom directory +cd public/dom + +# install puppeteer +npm i puppeteer +``` + +### Executing a test + +```bash +# run a test +SOLUTION_PATH=/user/you/piscine-repo node test.js exercise-name +``` + +The `SOLUTION_PATH` is the directory where the test should look +for your solution, usualy your piscine repository. + +The `exercise-name` argument should match exactly the name of an +exercise, not including `.js` diff --git a/test-images/test-dom/call-it_test.js b/test-images/test-dom/call-it_test.js new file mode 100644 index 0000000..e6aaea8 --- /dev/null +++ b/test-images/test-dom/call-it_test.js @@ -0,0 +1,19 @@ +export const tests = [] + +tests.push(async ({ page, eq }) => { + // check the face + + return eq.$('section#face', { textContent: '' }) +}) + +tests.push(async ({ page, eq }) => { + // check the upper-body + + return eq.$('section#upper-body', { textContent: '' }) +}) + +tests.push(async ({ page, eq }) => { + // check the lower-body, my favorite part + + return eq.$('section#lower-body', { textContent: '' }) +}) diff --git a/test-images/test-dom/class-it_test.js b/test-images/test-dom/class-it_test.js new file mode 100644 index 0000000..f84a445 --- /dev/null +++ b/test-images/test-dom/class-it_test.js @@ -0,0 +1,36 @@ +export const tests = [] + +tests.push(async ({ page, eq }) => { + // check the class 'eye' has been declared properly in the CSS + eq.css('.eye', { + width: '60px', + height: '60px', + backgroundColor: 'red', + borderRadius: '50%', + }) +}) + +tests.push(async ({ page, eq }) => { + // check the class 'arm' has been declared properly in the CSS + eq.css('.arm', { backgroundColor: 'aquamarine' }) +}) + +tests.push(async ({ page, eq }) => { + // check the class 'leg' has been declared properly in the CSS + eq.css('.leg', { backgroundColor: 'dodgerblue' }) +}) + +tests.push(async ({ page, eq }) => { + // check the class 'body-member' has been declared properly in the CSS + eq.css('.body-member', { width: '50px', margin: '30px' }) +}) + +tests.push(async ({ page, eq }) => { + // check that the targetted elements have the correct class names + await eq.$('p#eye-left', { className: 'eye' }) + await eq.$('p#eye-right', { className: 'eye' }) + await eq.$('div#arm-left', { className: 'arm body-member' }) + await eq.$('div#arm-right', { className: 'arm body-member' }) + await eq.$('div#leg-left', { className: 'leg body-member' }) + await eq.$('div#leg-right', { className: 'leg body-member' }) +}) diff --git a/test-images/test-dom/colorful-arms_test.js b/test-images/test-dom/colorful-arms_test.js new file mode 100644 index 0000000..6d8582d --- /dev/null +++ b/test-images/test-dom/colorful-arms_test.js @@ -0,0 +1,72 @@ +export const tests = [] + +tests.push(async ({ eq, page }) => { + // Check if the button with id 'arm-color' exists + const buttonExists = await page.$('button#arm-color') + eq(!!buttonExists, true) +}) + +tests.push(async ({ eq, page }) => { + // Check if the left and right arms exist + const leftArmExists = await page.$('#arm-left') + const rightArmExists = await page.$('#arm-right') + eq(!!leftArmExists && !!rightArmExists, true) +}) + +tests.push(async ({ eq, page }) => { + // Get the initial background colors of the arms + const initialLeftArmColor = await page.$eval( + '#arm-left', + node => getComputedStyle(node).backgroundColor, + ) + const initialRightArmColor = await page.$eval( + '#arm-right', + node => getComputedStyle(node).backgroundColor, + ) + + // Click the 'arm-color' button + const button = await page.$('button#arm-color') + await button.click() + + // Get the new background colors of the arms after clicking the button + const newLeftArmColor = await page.$eval( + '#arm-left', + node => getComputedStyle(node).backgroundColor, + ) + const newRightArmColor = await page.$eval( + '#arm-right', + node => getComputedStyle(node).backgroundColor, + ) + + // Check if the colors have changed and are now different from the initial colors + eq(initialLeftArmColor !== newLeftArmColor, true) + eq(initialRightArmColor !== newRightArmColor, true) + eq(newLeftArmColor, newRightArmColor) // Check if both arms have the same color +}) + +tests.push(async ({ eq, page }) => { + // Click the 'arm-color' button multiple times to ensure the colors keep changing + const button = await page.$('button#arm-color') + + const armColors = [] + for (let i = 0; i < 3; i++) { + await button.click() + const leftArmColor = await page.$eval( + '#arm-left', + node => getComputedStyle(node).backgroundColor, + ) + const rightArmColor = await page.$eval( + '#arm-right', + node => getComputedStyle(node).backgroundColor, + ) + armColors.push({ leftArmColor, rightArmColor }) + } + + // Check if the colors are different in each click + eq(new Set(armColors.map(c => c.leftArmColor)).size, armColors.length) + eq(new Set(armColors.map(c => c.rightArmColor)).size, armColors.length) + // Check if the arms always have the same color after each click + armColors.forEach(colorPair => + eq(colorPair.leftArmColor, colorPair.rightArmColor), + ) +}) diff --git a/test-images/test-dom/colorful-legs_test.js b/test-images/test-dom/colorful-legs_test.js new file mode 100644 index 0000000..767f57a --- /dev/null +++ b/test-images/test-dom/colorful-legs_test.js @@ -0,0 +1,56 @@ +export const tests = [] + +tests.push(async ({ eq, page }) => { + // Click on the button to change the robot's leg colors + const button = await page.$('button#leg-color') + await button.click() + + // Get the new colors of both legs + const legLeftColor = await page.$eval( + '#leg-left', + node => getComputedStyle(node).backgroundColor, + ) + const legRightColor = await page.$eval( + '#leg-right', + node => getComputedStyle(node).backgroundColor, + ) + + // Check if both legs have been assigned the same new color + eq(legLeftColor, legRightColor) +}) + +tests.push(async ({ eq, page }) => { + // Get the initial colors of the legs before clicking the button + const initialLegLeftColor = await page.$eval( + '#leg-left', + node => getComputedStyle(node).backgroundColor, + ) + const _initialLegRightColor = await page.$eval( + '#leg-right', + node => getComputedStyle(node).backgroundColor, + ) + + // Click on the button to change the robot's leg colors + const button = await page.$('button#leg-color') + await button.click() + + // Get the new colors of both legs + const newLegLeftColor = await page.$eval( + '#leg-left', + node => getComputedStyle(node).backgroundColor, + ) + const newLegRightColor = await page.$eval( + '#leg-right', + node => getComputedStyle(node).backgroundColor, + ) + + // Check if both legs have been assigned the same new color + eq(newLegLeftColor, newLegRightColor) + + // Ensure the new color is different from the initial color + eq( + newLegLeftColor !== initialLegLeftColor, + true, + 'The color of the legs should be different from the initial color', + ) +}) diff --git a/test-images/test-dom/embedded-organs_test.js b/test-images/test-dom/embedded-organs_test.js new file mode 100644 index 0000000..f3c594d --- /dev/null +++ b/test-images/test-dom/embedded-organs_test.js @@ -0,0 +1,90 @@ +export const tests = [] + +tests.push(async ({ page, eq }) => { + // check that the HTML structure is correct & elements are nested properly + const elements = await page.$$eval('body', nodes => { + const toNode = el => { + const node = {} + node.tag = el.tagName.toLowerCase() + node.id = el.id + if (el.children.length) { + node.children = [...el.children].map(toNode) + } + return node + } + return [...nodes[0].children].map(toNode) + }) + eq(expectedStructure, elements) +}) + +tests.push(async ({ page, eq }) => { + // check the section selector style has been updated properly + eq.css('section', { + display: 'flex', + justifyContent: 'center', + }) +}) + +tests.push(async ({ page, eq }) => { + // check if the provided CSS has been correctly copy pasted + eq.css('div, p', { + border: '1px solid black', + padding: '10px', + margin: '0px', + borderRadius: '30px', + }) + + eq.css('#face', { alignItems: 'center' }) + + eq.css('#eyes', { + display: 'flex', + backgroundColor: 'yellow', + justifyContent: 'space-between', + alignItems: 'center', + borderRadius: '50px', + width: '200px', + }) + + eq.css('#torso', { + width: '200px', + backgroundColor: 'violet', + }) +}) + +const expectedStructure = [ + { + tag: 'section', + + id: 'face', + children: [ + { + tag: 'div', + + id: 'eyes', + children: [ + { tag: 'p', id: 'eye-left' }, + { tag: 'p', id: 'eye-right' }, + ], + }, + ], + }, + { + tag: 'section', + + id: 'upper-body', + children: [ + { tag: 'div', id: 'arm-left' }, + { tag: 'div', id: 'torso' }, + { tag: 'div', id: 'arm-right' }, + ], + }, + { + tag: 'section', + + id: 'lower-body', + children: [ + { tag: 'div', id: 'leg-left' }, + { tag: 'div', id: 'leg-right' }, + ], + }, +] diff --git a/test-images/test-dom/entrypoint.sh b/test-images/test-dom/entrypoint.sh new file mode 100755 index 0000000..e8c31a0 --- /dev/null +++ b/test-images/test-dom/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +set -e + +cd /app +node --no-warnings --unhandled-rejections=strict test.js "${EXERCISE}" diff --git a/test-images/test-dom/first-hello_test.js b/test-images/test-dom/first-hello_test.js new file mode 100644 index 0000000..770e2a8 --- /dev/null +++ b/test-images/test-dom/first-hello_test.js @@ -0,0 +1,56 @@ +export const tests = [] + +tests.push(async ({ eq, page }) => { + // Check if the class 'words' has been added in the CSS + await eq.css('.words', { textAlign: 'center', fontFamily: 'sans-serif' }) +}) + +tests.push(async ({ eq, page }) => { + // Check if the torso element is initially empty + const isEmpty = await page.$eval('#torso', node => !node.children.length) + eq(isEmpty, true) +}) + +tests.push(async ({ eq, page }) => { + // Click on the button + const button = await page.$('button#speak-button') + await button.click() + + // Check if a new text element is added in the torso + const torsoChildren = await page.$eval('#torso', node => + [...node.children].map(child => ({ + tag: child.tagName, + text: child.textContent, + class: child.className, + })), + ) + eq(torsoChildren, [textNode]) +}) + +tests.push(async ({ eq, page }) => { + // Click a second time on the button + const button = await page.$('button#speak-button') + await button.click() + + // Check if the text element is removed from the torso + const isEmpty = await page.$eval('#torso', node => !node.children.length) + eq(isEmpty, true) +}) + +tests.push(async ({ eq, page }) => { + // Click the button once more to ensure the text is added again + const button = await page.$('button#speak-button') + await button.click() + + // Check if a new text element is added in the torso + const torsoChildren = await page.$eval('#torso', node => + [...node.children].map(child => ({ + tag: child.tagName, + text: child.textContent, + class: child.className, + })), + ) + eq(torsoChildren, [textNode]) +}) + +const textNode = { tag: 'DIV', text: 'Hello World', class: 'words' } diff --git a/test-images/test-dom/first-move_test.js b/test-images/test-dom/first-move_test.js new file mode 100644 index 0000000..a41d7c5 --- /dev/null +++ b/test-images/test-dom/first-move_test.js @@ -0,0 +1,40 @@ +export const tests = [] + +tests.push(async ({ eq, page }) => { + // check the JS script has a valid src + const source = await page.$eval( + 'script', + node => node.src.includes('.js') && node.src, + ) + if (!source.length) throw Error('missing script src') +}) + +tests.push(async ({ eq, page }) => { + // check the class 'eye-closed' has been added in the CSS + eq.css('.eye-closed', { + height: '4px', + padding: '0px 5px', + borderRadius: '10px', + }) +}) + +tests.push(async ({ eq, page }) => { + // check the class of left eye before the JS is loaded + await page.setJavaScriptEnabled(false) + await page.reload() + await eq.$('p#eye-left', { className: 'eye' }) +}) + +tests.push(async ({ eq, page }) => { + // check the class of left eye has been updated after the JS is loaded + await page.setJavaScriptEnabled(true) + await page.reload() + await eq.$('p#eye-left', { className: 'eye eye-closed' }) + + // check the background color of left eye has changed after the JS is loaded + const eyeLeftBg = await page.$eval( + '#eye-left', + node => node.style.backgroundColor, + ) + eq(eyeLeftBg, 'black') +}) diff --git a/test-images/test-dom/first-wink_test.js b/test-images/test-dom/first-wink_test.js new file mode 100644 index 0000000..9b7784b --- /dev/null +++ b/test-images/test-dom/first-wink_test.js @@ -0,0 +1,45 @@ +export const tests = [] + +tests.push(async ({ eq, page }) => { + // check the initial class name of the eye left + const eyeLeft = await page.$eval('#eye-left', node => node.className) + eq(eyeLeft, 'eye') + + // check that the text of the button says 'close' + const buttonText = await page.$eval('button', node => node.textContent) + eq(buttonText, 'Click to close the left eye') +}) + +tests.push(async ({ eq, page }) => { + // click the button to close the left eye + const button = await page.$('button') + button.click() + + // check that the class has been added + await page.waitForSelector('#eye-left.eye.eye-closed', { timeout: 150 }) + + // check the background color has changed + await eq.$('#eye-left.eye.eye-closed', { + style: { backgroundColor: 'black' }, + }) + + // check that the text of the button changed to 'open' + await eq.$('button', { textContent: 'Click to open the left eye' }) +}) + +tests.push(async ({ eq, page }) => { + // click the button a second time to open the left eye + const button = await page.$('button') + button.click() + + // check that the class has been removed + await page.waitForSelector('#eye-left.eye:not(.eye-closed)', { timeout: 150 }) + + // check the background color has changed + await eq.$('#eye-left.eye:not(.eye-closed)', { + style: { backgroundColor: 'red' }, + }) + + // check that the text of the button changed to 'close' + await eq.$('button', { textContent: 'Click to close the left eye' }) +}) diff --git a/test-images/test-dom/package.json b/test-images/test-dom/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/test-images/test-dom/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test-images/test-dom/select-then-style_test.js b/test-images/test-dom/select-then-style_test.js new file mode 100644 index 0000000..0191172 --- /dev/null +++ b/test-images/test-dom/select-then-style_test.js @@ -0,0 +1,44 @@ +export const tests = [] + +tests.push(async ({ eq }) => { + // check the CSS stylesheet is linked in the head tag + + await eq.$('head link', { + rel: 'stylesheet', + href: 'http://localhost:9898/select-then-style/select-then-style.css', + }) +}) + +tests.push(async ({ eq }) => { + // check the universal selector has been declared properly + + await eq.css('*', { + margin: '0px', + opacity: '0.85', + boxSizing: 'border-box', + }) +}) + +tests.push(async ({ eq }) => { + // check that the body was styled + + await eq.css('body', { height: '100vh' }) +}) + +tests.push(async ({ eq }) => { + // check that sections elements are styled + + await eq.css('section', { + padding: '20px', + width: '100%', + height: 'calc(33.3333%)', + }) +}) + +tests.push(async ({ eq }) => { + // check that the individual sections are styled + + await eq.css('#face', { backgroundColor: 'cyan' }) + await eq.css('#upper-body', { backgroundColor: 'blueviolet' }) + await eq.css('#lower-body', { backgroundColor: 'lightsalmon' }) +}) diff --git a/test-images/test-dom/test.js b/test-images/test-dom/test.js new file mode 100644 index 0000000..5aa0c27 --- /dev/null +++ b/test-images/test-dom/test.js @@ -0,0 +1,174 @@ +import { deepStrictEqual } from 'node:assert' +import fs from 'node:fs' +import http from 'node:http' +import path from 'node:path' +import puppeteer from 'puppeteer' + +const exercise = process.argv[2] +if (!exercise) throw Error(`usage: node test EXERCISE_NAME`) +const PORT = 9898 +const config = { + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + + // This will write shared memory files into /tmp instead of /dev/shm, + // because Docker’s default for /dev/shm is 64MB + '--disable-dev-shm-usage', + ], + headless: !process.env.SOLUTION_PATH, +} + +const solutionPath = process.env.SOLUTION_PATH || '/jail/student' +const mediaTypes = { + jpg: 'image/jpeg', + png: 'image/png', + html: 'text/html', + css: 'text/css', + js: 'application/javascript', + json: 'application/json', +} + +const random = (min, max = min) => { + max === min && (min = 0) + min = Math.ceil(min) + return Math.floor(Math.random() * (Math.floor(max) - min + 1)) + min +} + +const rgbToHsl = rgbStr => { + const [r, g, b] = rgbStr.slice(4, -1).split(',').map(Number) + const max = Math.max(r, g, b) + const min = Math.min(r, g, b) + const l = (max + min) / ((0xff * 2) / 100) + + if (max === min) return [0, 0, l] + + const d = max - min + const s = (d / (l > 50 ? 0xff * 2 - max - min : max + min)) * 100 + if (max === r) return [((g - b) / d + (g < b && 6)) * 60, s, l] + return max === g + ? [((b - r) / d + 2) * 60, s, l] + : [((r - g) / d + 4) * 60, s, l] +} + +const pathMap = { + [`/${exercise}/${exercise}.js`]: path.join(solutionPath, `${exercise}.js`), + [`/${exercise}/${exercise}.css`]: path.join(solutionPath, `${exercise}.css`), +} + +const ifNotExists = (p, fn) => { + try { + fs.statSync(p) + } catch (err) { + if (err.code !== 'ENOENT') throw err + fn() + } +} + +ifNotExists(`./subjects/${exercise}/${exercise}.html`, () => { + const indexPath = path.join(solutionPath, `${exercise}.html`) + pathMap[`/${exercise}/${exercise}.html`] = indexPath + ifNotExists(indexPath, () => { + console.error(`missing student ${exercise}.html file`) + process.exit(1) + }) +}) + +const server = http + .createServer(({ url, method }, response) => { + console.log(`${method} ${url}`) + if (url.endsWith('/favicon.ico')) return response.end() + const filepath = pathMap[url] || path.join('./subjects', url) + const ext = path.extname(filepath) + response.setHeader('Content-Type', mediaTypes[ext.slice(1)] || 'text/plain') + + const _stream = fs + .createReadStream(filepath) + .pipe(response) + .once('error', err => { + console.log(err) + response.statusCode = 500 // handle 404 ? + response.end('oopsie') + }) + }) + .listen(PORT, async err => { + let browser, + code = 0 + try { + err && (console.error(err.stack) || process.exit(1)) + const { setup = () => {}, tests } = await import(`./${exercise}_test.js`) + browser = await puppeteer.launch(config) + + const [page] = await browser.pages() + await page.goto(`http://localhost:${PORT}/${exercise}/${exercise}.html`) + deepStrictEqual.$ = async (selector, props) => { + const _keys = Object.keys(props) + const extractProps = (node, props) => { + const fromProps = (a, b) => + Object.fromEntries( + Object.keys(b).map(k => [ + k, + typeof b[k] === 'object' ? fromProps(a[k], b[k]) : a[k], + ]), + ) + return fromProps(node, props) + } + const domProps = await page.$eval(selector, extractProps, props) + return deepStrictEqual(props, domProps) + } + + deepStrictEqual.css = async (selector, props) => { + const cssProps = await page.evaluate( + (selector, props) => { + const styles = Object.fromEntries( + [...document.styleSheets].flatMap(({ cssRules }) => + [...cssRules].map(r => [r.selectorText, r.style]), + ), + ) + + if (!styles[selector]) { + throw Error(`css ${selector} did not match any declarations`) + } + + return Object.fromEntries( + Object.keys(props).map(k => [k, styles[selector][k]]), + ) + }, + selector, + props, + ) + + return deepStrictEqual(props, cssProps) + } + + const baseContext = { + page, + browser, + eq: deepStrictEqual, + random, + rgbToHsl, + } + const context = await setup(baseContext) + + browser + .defaultBrowserContext() + .overridePermissions(`http://localhost:${PORT}`, ['clipboard-read']) + + for (const [n, test] of tests.entries()) { + try { + await test({ ...baseContext, ...context }) + } catch (err) { + console.log(`test #${n} failed:`) + console.log(test.toString()) + throw err + } + } + } catch (err) { + code = 1 + console.log(err.stack) + } finally { + await browser?.close() + server.close() + process.exit(code) + } + }) diff --git a/test-images/test-dom/the-skeleton_test.js b/test-images/test-dom/the-skeleton_test.js new file mode 100644 index 0000000..439d2dd --- /dev/null +++ b/test-images/test-dom/the-skeleton_test.js @@ -0,0 +1,44 @@ +export const tests = [] + +tests.push(async ({ page, eq }) => { + // check that the title tag is present & is set with some text + const title = await page.$eval('title', node => node.textContent) + if (!title.length) throw Error('missing title') +}) + +tests.push(async ({ page, eq }) => { + // check that the title tag is set with text from the given list + const title = await page.$eval('title', node => node.textContent) + if ( + title !== 'invisibility' && + title !== 'light-speed' && + title !== 'super-strength' && + title !== 'advanced-healing' && + title !== 'mind-link' + ) { + throw Error('wrong title, pick one of the list') + } + // invisibility + // light-speed + // super-strength + // advanced-healing + // mind-link +}) + +tests.push(async ({ page, eq }) => { + // check the face + + return eq.$('section:nth-child(1)', { textContent: 'face' }) +}) + +tests.push(async ({ page, eq }) => { + // check the upper-body + + return eq.$('section:nth-child(2)', { textContent: 'upper-body' }) +}) + +tests.push(async ({ page, eq }) => { + // check the lower-body + + return eq.$('section:nth-child(3)', { textContent: 'lower-body' }) +}) diff --git a/test-images/test-dom/yarn.lock b/test-images/test-dom/yarn.lock new file mode 100644 index 0000000..830a863 --- /dev/null +++ b/test-images/test-dom/yarn.lock @@ -0,0 +1,315 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@*": + version "14.0.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.13.tgz#ee1128e881b874c371374c1f72201893616417c9" + integrity sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA== + +"@types/yauzl@^2.9.1": + version "2.9.1" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af" + integrity sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA== + dependencies: + "@types/node" "*" + +agent-base@5: + version "5.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" + integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +bl@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.2.tgz#52b71e9088515d0606d9dd9cc7aa48dc1f98e73a" + integrity sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + +buffer@^5.2.1, buffer@^5.5.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" + integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +debug@4, debug@^4.1.0, debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +extract-zip@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + dependencies: + pend "~1.2.0" + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +get-stream@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" + integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== + dependencies: + pump "^3.0.0" + +glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +https-proxy-agent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" + integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg== + dependencies: + agent-base "5" + debug "4" + +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +mime@^2.0.3: + version "2.4.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + +progress@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +proxy-from-env@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +puppeteer-core@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-3.3.0.tgz#6178a6a0f6efa261cd79e42e34ab0780d8775f0d" + integrity sha512-hynQ3r0J/lkGrKeBCqu160jrj0WhthYLIzDQPkBxLzxPokjw4elk1sn6mXAian/kfD2NRzpdh9FSykxZyL56uA== + dependencies: + debug "^4.1.0" + extract-zip "^2.0.0" + https-proxy-agent "^4.0.0" + mime "^2.0.3" + progress "^2.0.1" + proxy-from-env "^1.0.0" + rimraf "^3.0.2" + tar-fs "^2.0.0" + unbzip2-stream "^1.3.3" + ws "^7.2.3" + +readable-stream@^3.1.1, readable-stream@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +tar-fs@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" + integrity sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.0.0" + +tar-stream@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.2.tgz#6d5ef1a7e5783a95ff70b69b97455a5968dc1325" + integrity sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q== + dependencies: + bl "^4.0.1" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +unbzip2-stream@^1.3.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +ws@^7.2.3: + version "7.3.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" + integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w== + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" diff --git a/test-images/test-js/Dockerfile b/test-images/test-js/Dockerfile new file mode 100644 index 0000000..05a241e --- /dev/null +++ b/test-images/test-js/Dockerfile @@ -0,0 +1,22 @@ +FROM docker.01-edu.org/alpine:3.17.0 + +# Installs latest Chromium package. +RUN apk add --no-cache \ + chromium \ + nss \ + freetype \ + harfbuzz \ + ca-certificates \ + ttf-freefont \ + nodejs \ + yarn + +# Tell Puppeteer to skip installing Chrome. We'll be using the installed package. +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \ + PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser + +RUN yarn add puppeteer@15.3.2 + +WORKDIR /app +COPY . . +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/test-images/test-js/README.md b/test-images/test-js/README.md new file mode 100644 index 0000000..4e1926d --- /dev/null +++ b/test-images/test-js/README.md @@ -0,0 +1,31 @@ +# JS + +Tests that use node to do JavaScript exercises + +## Run test locally + +### Installation + +> You need node version 14+ + +```bash +# Clone the repo +git clone https://github.com/01-edu/public.git + +# go into the dom directory +cd public/js/tests +``` + +### Executing a test + +```bash +# run a test +node test.mjs /user/you/piscine-repo exercise-name +``` + +The first argument `/user/you/piscine-repo` is the directory +where the test should look for your solution, +usualy your piscine repository. + +The second argument `exercise-name` should match exactly +the name of an exercise, not including `.js` diff --git a/test-images/test-js/entrypoint.sh b/test-images/test-js/entrypoint.sh new file mode 100755 index 0000000..6e89170 --- /dev/null +++ b/test-images/test-js/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +set -e + +if test "$CODE_EDITOR_RUN_ONLY" = true; then + node "/jail/student/${EXERCISE}.js" "$@" + exit +fi + +node /app/test.mjs "/jail/student" "${EXERCISE}" diff --git a/test-images/test-js/test.mjs b/test-images/test-js/test.mjs new file mode 100644 index 0000000..4630700 --- /dev/null +++ b/test-images/test-js/test.mjs @@ -0,0 +1,347 @@ +import { deepStrictEqual } from 'node:assert' +import { readFile, writeFile } from 'node:fs/promises' +import http from 'node:http' +import { tmpdir } from 'node:os' +import { dirname, extname, join as joinPath } from 'node:path' +import { fileURLToPath } from 'node:url' +import puppeteer from 'puppeteer' + +global.window = global +global._fetch = fetch +global.fetch = _url => { + // this is a fake implementation of fetch for the tester + const accessBody = async () => { + throw Error('body unavailable') + } + return { + ok: false, + type: 'basic', + status: 500, + statusText: 'Internal Server Error', + json: accessBody, + text: accessBody, + } +} + +const wait = delay => new Promise(s => setTimeout(s, delay)) +const fail = fn => { + try { + fn() + } catch (_err) { + return true + } +} +const upperFirst = str => str[0].toUpperCase() + str.slice(1) +const randStr = (n = 7) => Math.random().toString(36).slice(2, n) +const between = (min, max) => { + max || ((max = min), (min = 0)) + return Math.floor(Math.random() * (max - min) + min) +} + +const props = [String, Array] + .flatMap(({ prototype }) => + Object.getOwnPropertyNames(prototype).map(key => ({ + key, + value: prototype[key], + src: prototype, + })), + ) + .filter(p => typeof p.value === 'function') + +const eq = (a, b) => { + const changed = [] + for (const p of props) { + !p.src[p.key] && (changed[changed.length] = p) + } + for (const p of changed) { + p.src[p.key] = p.value + } + deepStrictEqual(a, b) + for (const p of changed) { + p.src[p.key] = undefined + } + return true +} + +const [solutionPath, name] = process.argv.slice(2) + +const tools = { eq, fail, wait, randStr, between, upperFirst } +const fatal = (...args) => { + console.error(...args) + process.exit(1) +} + +solutionPath || + fatal('missing solution-path, usage:\nnode test solution-path exercise-name') +name || fatal('missing exercise, usage:\nnode test solution-path exercise-name') + +const ifNoEnt = fn => err => { + if (err.code !== 'ENOENT') throw err + fn(err) +} + +const root = dirname(fileURLToPath(import.meta.url)) +const read = (filename, description) => + readFile(filename, 'utf8').catch( + ifNoEnt(() => fatal(`Missing ${description} for ${name}`)), + ) + +const modes = { '.js': 'function', '.mjs': 'node', '.json': 'inline' } +const readTest = filename => + readFile(filename, 'utf8').then(test => ({ + test, + mode: modes[extname(filename)], + })) + +const stackFmt = (err, url) => { + for (const p of props) { + p.src[p.key] = p.value + } + if (err instanceof Error) return err.stack.split(url).join(`${name}.js`) + throw Error( + `Unexpected type thrown: ${typeof err}. usage: throw Error('my message')`, + ) +} + +const any = arr => + new Promise(async (s, f) => { + let firstError + const setError = err => firstError || (firstError = err) + await Promise.all(arr.map(p => p.then(s, setError))) + f(firstError) + }) + +const testNode = async () => { + const path = `${solutionPath}/${name}.mjs` + return { + path, + url: joinPath(root, `${name}_test.mjs`), + code: await read(path, 'student solution'), + } +} + +const runInlineTests = async ({ json }) => { + const restore = new Set() + const _equal = deepStrictEqual + const _saveArguments = (src, key) => { + const savedArgs = [] + const fn = src[key] + src[key] = (...args) => { + savedArgs.push(args) + return fn(...args) + } + + restore.add(() => (src[key] = fn)) + + return savedArgs + } + + const logs = [] + console.log = (...args) => logs.push(args) + const die = (...args) => { + logs.forEach(logArgs => console.info(...logArgs)) + fatal(...args) + } + + const solution = await loadAndSanitizeSolution() + for (const { description, code } of JSON.parse(json)) { + logs.length = 0 + const [provided, tests] = code.includes('// Your code') + ? code.split('// Your code') + : ['', code] + + const fullCode = ` +${provided ? '// Provided setup' : ''} +${provided.trim()} + +// Your code +${solution.code.trim()}; + +// The tests +${tests.trim()};`.trim() + + try { + eval(fullCode) + console.info(`${description}:`, 'PASS') + } catch (err) { + console.info(`${description}:`, 'FAIL') + console.info('\n======= Error ======') + console.info(' ->', err.message, '\n') + console.info('\n======= Code =======') + die(fullCode) + } + } +} + +const loadAndSanitizeSolution = async () => { + const path = `${solutionPath}/${name}.js` + const rawCode = await read(path, 'student solution') + + // this is a very crude and basic removal of comments + // since checking code is only use to prevent cheating + // it's not that important if it doesn't work 100% of the time. + const code = rawCode.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '').trim() + if (code.includes('import')) fatal('import keyword not allowed') + return { code, rawCode, path } +} + +const runTests = async ({ url, path, code }) => { + const { setup, tests } = await import(url).catch(err => + fatal(`Unable to execute ${name}, error:\n${stackFmt(err, url)}`), + ) + + Object.assign(tools, { code, path }) + tools.ctx = (await setup?.(tools)) || {} + const isDOM = name.endsWith('-dom') + if (isDOM) { + Object.assign(tools, await prepareForDOM({ code })) + } + let timeout + for (const [i, t] of tests.entries()) { + try { + const waitWithTimeout = Promise.race([ + t(tools), + new Promise((_s, f) => { + timeout = setTimeout(f, 60000, Error('Time limit reached (1min)')) + }), + ]) + if (!(await waitWithTimeout) && !isDOM) { + throw Error('Test failed') + } + } catch (err) { + console.info(`test #${i + 1} failed:\n${t.toString()}\n`) + fatal(stackFmt(err, url)) + } finally { + clearTimeout(timeout) + } + } + console.info(`${name} passed (${tests.length} tests)`) +} + +// add puppeteer tests as JS language: +const PORT = 9898 +const config = { + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + + // This will write shared memory files into /tmp instead of /dev/shm, + // because Docker’s default for /dev/shm is 64MB + '--disable-dev-shm-usage', + ], + headless: !process.env.DEBUG_PUPPETTEER, +} + +// LEGACY random, use between instead (only used by dom exercise, to be replaced) +const random = (min, max = min) => { + max === min && (min = 0) + min = Math.ceil(min) + return Math.floor(Math.random() * (Math.floor(max) - min + 1)) + min +} + +const rgbToHsl = rgbStr => { + const [r, g, b] = rgbStr.slice(4, -1).split(',').map(Number) + const max = Math.max(r, g, b) + const min = Math.min(r, g, b) + const l = (max + min) / ((0xff * 2) / 100) + + if (max === min) return [0, 0, l] + + const d = max - min + const s = (d / (l > 50 ? 0xff * 2 - max - min : max + min)) * 100 + if (max === r) return [((g - b) / d + (g < b && 6)) * 60, s, l] + return max === g + ? [((b - r) / d + 2) * 60, s, l] + : [((r - g) / d + 4) * 60, s, l] +} + +const prepareForDOM = ({ code }, server) => + new Promise((s, f) => + (server = http.createServer(({ url, method }, response) => { + console.info(`${method} ${url}`) + // Loading either the `index.html` or the js code (student solution) + response.setHeader('Content-Type', 'text/html') + return response.end(``) + })).listen(PORT, async listenErr => { + if (listenErr) return f(listenErr) + try { + const browser = await puppeteer.launch(config) + const [page] = await browser.pages() + await page.goto(`http://localhost:${PORT}/index.html`) + deepStrictEqual.$ = async (selector, props) => { + const _keys = Object.keys(props) + const extractProps = (node, props) => { + const fromProps = (a, b) => + Object.fromEntries( + Object.keys(b).map(k => [ + k, + typeof b[k] === 'object' ? fromProps(a[k], b[k]) : a[k], + ]), + ) + return fromProps(node, props) + } + const domProps = await page.$eval(selector, extractProps, props) + return deepStrictEqual(props, domProps) + } + + deepStrictEqual.css = async (selector, props) => { + const cssProps = await page.evaluate( + (selector, props) => { + const styles = Object.fromEntries( + [...document.styleSheets].flatMap(({ cssRules }) => + [...cssRules].map(r => [r.selectorText, r.style]), + ), + ) + + if (!styles[selector]) { + throw Error(`css ${selector} did not match any declarations`) + } + + return Object.fromEntries( + Object.keys(props).map(k => [k, styles[selector][k]]), + ) + }, + selector, + props, + ) + + return deepStrictEqual(props, cssProps) + } + + browser + .defaultBrowserContext() + .overridePermissions(`http://localhost:${PORT}`, ['clipboard-read']) + + s({ page, browser, random, rgbToHsl, eq: deepStrictEqual, server }) + } catch (err) { + f(err) + } + }), + ) + +const main = async () => { + const { test, mode } = await any([ + readTest(joinPath(root, `${name}.json`)), + readTest(joinPath(root, `${name}_test.js`)), + readTest(joinPath(root, `${name}_test.mjs`)), + ]).catch(ifNoEnt(_err => fatal(`Missing test for ${name}`))) + + if (mode === 'node') return runTests(await testNode()) + if (mode === 'inline') return runInlineTests({ json: test }) + + const { rawCode, code, path } = await loadAndSanitizeSolution() + const parts = test.split('// /*/ // ⚡') + const [inject, testCode] = parts.length < 2 ? ['', test] : parts + const combined = `${inject.trim()}\n${rawCode + .replace(inject.trim(), '') + .trim()}\n;${testCode.trim()}\n` + + const url = `${tmpdir()}/${name}.mjs` + await writeFile(url, combined) + return runTests({ path, code, url }) +} + +main().then( + () => process.exit(0), + err => fatal(err?.stack || Error('').stack), +) diff --git a/test-images/test-js/tests/declare-everything.json b/test-images/test-js/tests/declare-everything.json new file mode 100644 index 0000000..cb85b76 --- /dev/null +++ b/test-images/test-js/tests/declare-everything.json @@ -0,0 +1,14 @@ +[ + { + "description": "As a number, seven value is 7", + "code": "equal(Number(seven), 7)" + }, + { + "description": "As a number, seventySeven value is 77", + "code": "equal(Number(seventySeven), 77)" + }, + { + "description": "Somehow, the type of seven and seventySeven value must be strings", + "code": "equal(typeof seven, 'string')\nequal(typeof seventySeven, 'string')" + } +] diff --git a/test-images/test-js/tests/first-function.json b/test-images/test-js/tests/first-function.json new file mode 100644 index 0000000..ce264ea --- /dev/null +++ b/test-images/test-js/tests/first-function.json @@ -0,0 +1,22 @@ +[ + { + "description": "ask is defined and is a function", + "code": "equal(typeof ask, 'function')" + }, + { + "description": "reply is defined and is a function", + "code": "equal(typeof reply, 'function')" + }, + { + "description": "ask works and is called", + "code": "const args = saveArguments(console, 'log')\n\n// Your code\n\nequal(args[0], ['What is my purpose ?'])" + }, + { + "description": "reply works and is called too", + "code": "const args = saveArguments(console, 'log')\n\n// Your code\n\nequal(args, [['What is my purpose ?'], ['You pass butter.']])" + }, + { + "description": "calling reply and ask again relog the text.", + "code": "const args = saveArguments(console, 'log')\n\n// Your code\n\nequal(args[1], ['You pass butter.'])" + } +] diff --git a/test-images/test-js/tests/glance-on-power.json b/test-images/test-js/tests/glance-on-power.json new file mode 100644 index 0000000..c4797bb --- /dev/null +++ b/test-images/test-js/tests/glance-on-power.json @@ -0,0 +1,18 @@ +[ + { + "description": "Log a number in the console", + "code": "// If you see this code, it means that you failed the first tests.\n// each tests have it's own code to be tested that will appear if\n// your solution doesn't pass it, it is not here to help you.\n// While sometimes it may clarify the instructions\n// this specific test is complex and will most likely confuse you.\n\n// This is to save all the values that you console.log'd\nconst args = saveArguments(console, 'log')\n\n// This comment below will be replaced by your code\n// Your code\n\n// This is where we check that the value are expected.\n// It's pretty advanced code, you don't have to understand it\n// Do not try to use it for the solution, it will not help you.\nconst typeOfLoggedValues = args.flat().map((v) => typeof v)\nif (!typeOfLoggedValues.includes('number')) {\n // this is where we create the error message you see:\n throw Error('you must log a number')\n // that's what you should focus on trying to understand\n // the message, not `throw` or `Error` don't worry about\n // that, worry about showing a number in the console !\n}" + }, + { + "description": "Log a boolean in the console", + "code": "const args = saveArguments(console, 'log')\n\n// Your code\n\nconst typeOfLoggedValues = args.flat().map((v) => typeof v)\nif (!typeOfLoggedValues.includes('boolean')) {\n throw Error('you must log a boolean')\n}" + }, + { + "description": "Log a string in the console", + "code": "const args = saveArguments(console, 'log')\n\n// Your code\n\nconst typeOfLoggedValues = args.flat().map((v) => typeof v)\nif (!typeOfLoggedValues.includes('string')) {\n throw Error('you must log a string')\n}" + }, + { + "description": "Log the string Hello There! in the console", + "code": "const args = saveArguments(console, 'log')\n\n// Your code\n\nconst loggedValues = args.flat().join(' ')\nif (!loggedValues.includes('Hello There!')) {\n throw Error('you must log the text Hello There!')\n}" + } +] diff --git a/test-images/test-js/tests/good-recipe.json b/test-images/test-js/tests/good-recipe.json new file mode 100644 index 0000000..6ab0b1d --- /dev/null +++ b/test-images/test-js/tests/good-recipe.json @@ -0,0 +1,50 @@ +[ + { + "description": "Should work on mixed case", + "code": "let message = 'YoU cAn CaLl Me YoUr MaJeStY!'\nlet kevin = { age: 14 }\nlet stephanie = { age: 25 }\nlet martin = { age: 32 }\nlet alphabet = 'abcdefghijklmnopqrstuvwxyz'\n\n// Your code\n\nequal(noCaps, 'you can call me your majesty!')\nequal(allCaps, 'YOU CAN CALL ME YOUR MAJESTY!')" + }, + { + "description": "Should work on mixed case", + "code": "let message = `DoN'T tAlK aBoUt My MoMs, Yo`\nlet kevin = { age: 14 }\nlet stephanie = { age: 25 }\nlet martin = { age: 32 }\nlet alphabet = 'abcdefghijklmnopqrstuvwxyz'\n\n// Your code\n\nequal(noCaps, `don't talk about my moms, yo`)\nequal(allCaps, `DON'T TALK ABOUT MY MOMS, YO`)" + }, + { + "description": "oldestAge is a number", + "code": "let kevin = { age: 14 }\nlet stephanie = { age: 25 }\nlet martin = { age: 32 }\nlet alphabet = 'abcdefghijklmnopqrstuvwxyz'\nlet message = 'YoU cAn CaLl Me YoUr MaJeStY!'\n\n// Your code\n\nequal(typeof oldestAge, 'number')" + }, + { + "description": "oldestAge is the maximum value of the age property (martin)", + "code": "let kevin = { age: 14 }\nlet stephanie = { age: 25 }\nlet martin = { age: 32 }\nlet alphabet = 'abcdefghijklmnopqrstuvwxyz'\nlet message = 'YoU cAn CaLl Me YoUr MaJeStY!'\n\n// Your code\n\nequal(oldestAge, 32)" + }, + { + "description": "oldestAge is still the maximum value of the age property (kevin)", + "code": "let kevin = { age: 67 }\nlet stephanie = { age: 25 }\nlet martin = { age: 32 }\nlet alphabet = 'abcdefghijklmnopqrstuvwxyz'\nlet message = 'YoU cAn CaLl Me YoUr MaJeStY!'\n\n// Your code\n\nequal(oldestAge, 67)" + }, + { + "description": "oldestAge is still the maximum value of the age property (stephanie)", + "code": "let kevin = { age: 29 }\nlet stephanie = { age: 45 }\nlet martin = { age: 32 }\nlet alphabet = 'abcdefghijklmnopqrstuvwxyz'\nlet message = 'YoU cAn CaLl Me YoUr MaJeStY!'\n\n// Your code\n\nequal(oldestAge, 45)" + }, + { + "description": "cutFirst from the latin alphabet", + "code": "let alphabet = 'abcdefghijklmnopqrstuvwxyz'\nlet kevin = { age: 14 }\nlet stephanie = { age: 25 }\nlet martin = { age: 32 }\nlet message = 'YoU cAn CaLl Me YoUr MaJeStY!'\n\n// Your code\n\nequal(cutFirst, 'klmnopqrstuvwxyz')" + }, + { + "description": "cutFirst from the georgian alphabet", + "code": "let alphabet = 'აბგდევზთიკლმნოპჟრსტუფქღყშჩცძწჭხჯჰ'\nlet kevin = { age: 14 }\nlet stephanie = { age: 25 }\nlet martin = { age: 32 }\nlet message = 'YoU cAn CaLl Me YoUr MaJeStY!'\n\n// Your code\n\nequal(cutFirst, 'ლმნოპჟრსტუფქღყშჩცძწჭხჯჰ')" + }, + { + "description": "cutLast from the latin alphabet", + "code": "let alphabet = 'abcdefghijklmnopqrstuvwxyz'\nlet kevin = { age: 14 }\nlet stephanie = { age: 25 }\nlet martin = { age: 32 }\nlet message = 'YoU cAn CaLl Me YoUr MaJeStY!'\n\n// Your code\n\nequal(cutLast, 'abcdefghijklmnopqrstuvw')" + }, + { + "description": "cutLast from the greek alphabet", + "code": "let alphabet = 'αβγδεζηθικλμνξοπρστυφχψω'\nlet kevin = { age: 14 }\nlet stephanie = { age: 25 }\nlet martin = { age: 32 }\nlet message = 'YoU cAn CaLl Me YoUr MaJeStY!'\n\n// Your code\n\nequal(cutLast, 'αβγδεζηθικλμνξοπρστυφ')" + }, + { + "description": "cutFirstLast from the latin alphabet", + "code": "let alphabet = 'abcdefghijklmnopqrstuvwxyz'\nlet kevin = { age: 14 }\nlet stephanie = { age: 25 }\nlet martin = { age: 32 }\nlet message = 'YoU cAn CaLl Me YoUr MaJeStY!'\n\n// Your code\n\nequal(cutFirstLast, 'fghijklmnopqrst')" + }, + { + "description": "cutFirstLast from the armenian alphabet", + "code": "let alphabet = 'աբգդեզէըթժիլխծկհձղճմյնշոչպջռսվտրցւփքօֆուև'\nlet kevin = { age: 14 }\nlet stephanie = { age: 25 }\nlet martin = { age: 32 }\nlet message = 'YoU cAn CaLl Me YoUr MaJeStY!'\n\n// Your code\n\nequal(cutFirstLast, 'զէըթժիլխծկհձղճմյնշոչպջռսվտրցւփ')" + } +] diff --git a/test-images/test-js/tests/i-win-arguments.json b/test-images/test-js/tests/i-win-arguments.json new file mode 100644 index 0000000..e2cbaa1 --- /dev/null +++ b/test-images/test-js/tests/i-win-arguments.json @@ -0,0 +1,66 @@ +[ + { + "description": "battleCry is defined and is a function", + "code": "equal(typeof battleCry, 'function')" + }, + { + "description": "secretOrders is defined and is a function", + "code": "equal(typeof secretOrders, 'function')" + }, + { + "description": "battleCry has one and only one argument", + "code": "equal(battleCry.length, 1)" + }, + { + "description": "secretOrders has one and only one argument", + "code": "equal(secretOrders.length, 1)" + }, + { + "description": "battleCry shouts properly", + "code": "const args = saveArguments(console, 'log')\n// Your code\n\nbattleCry('attack')\nbattleCry('you shall not pass!')\n\nequal(args.flat(), ['ATTACK', 'YOU SHALL NOT PASS!'])" + }, + { + "description": "secretOrders whispers properly", + "code": "const args = saveArguments(console, 'log')\n\n// Your code\n\nsecretOrders('ExEcutE Order 66')\nsecretOrders('SILENCE')\n\nequal(args.flat(), ['execute order 66', 'silence'])" + }, + { + "description": "We can call both functions", + "code": "const args = saveArguments(console, 'log')\n\n// Your code\n\nsecretOrders('This Is The WAY')\nbattleCry('for the horde !')\n\nequal(args.flat(), ['this is the way', 'FOR THE HORDE !'])" + }, + { + "description": "duos is defined and is a function", + "code": "equal(typeof duos, 'function')" + }, + { + "description": "duos takes two arguments", + "code": "equal(duos.length, 2)" + }, + { + "description": "duos logs the expected result", + "code": "const args = saveArguments(console, 'log')\n\n// Your code\n\nduos('Batman', 'Robin')\nduos('Pinky', 'The Brain')\nduos('Bonnie', 'Clyde')\nduos('Mr.', 'Mrs.Smith')\n\nequal(\n args.map((arg) => arg.join(' ')),\n [\n 'Batman and Robin!',\n 'Pinky and The Brain!',\n 'Bonnie and Clyde!',\n 'Mr. and Mrs.Smith!',\n ],\n)" + }, + { + "description": "duosWork is defined and is a function", + "code": "equal(typeof duosWork, 'function')" + }, + { + "description": "duosWork takes three arguments", + "code": "equal(duosWork.length, 3)" + }, + { + "description": "duosWork logs the expected result", + "code": "const args = saveArguments(console, 'log')\n\n// Your code\n\nduosWork('Batman', 'Robin', 'protect Gotham')\nduosWork('Pinky', 'The Brain', 'want to conquer the world')\nduosWork('Bonnie', 'Clyde', 'escape the Police')\nduosWork('Mr.', 'Mrs.Smith', 'are the greatest spy couple')\n\nequal(\n args.map((arg) => arg.join(' ')),\n [\n 'Batman and Robin protect Gotham!',\n 'Pinky and The Brain want to conquer the world!',\n 'Bonnie and Clyde escape the Police!',\n 'Mr. and Mrs.Smith are the greatest spy couple!',\n ],\n)" + }, + { + "description": "passButter is defined and is a function", + "code": "equal(typeof passButter, 'function')" + }, + { + "description": "passButter returns The butter properly", + "code": "equal(passButter(), 'The butter')" + }, + { + "description": "calling passButter mulitple time should always return the butter", + "code": "equal(\n [passButter(), passButter(), passButter()],\n ['The butter', 'The butter', 'The butter'],\n)" + } +] diff --git a/test-images/test-js/tests/listed.json b/test-images/test-js/tests/listed.json new file mode 100644 index 0000000..d14a8ba --- /dev/null +++ b/test-images/test-js/tests/listed.json @@ -0,0 +1,58 @@ +[ + { + "description": "components variable must be an Array", + "code": "\n\nconst replaceComponents = ['sensor', 'battery', 'motor', 'brain']\nlet swapComponents = ['motor', 'battery']\nlet robotParts = [\n 'motor',\n 'sensor',\n 'camera',\n 'battery',\n // 'memory', ??\n]\n\n// Your code\nif (!Array.isArray(components)) {\n throw Error('Components must be an Array')\n}" + }, + { + "description": "components first element must be motor", + "code": "\n\nconst replaceComponents = ['sensor', 'battery', 'motor', 'brain']\nlet swapComponents = ['motor', 'battery']\nlet robotParts = [\n 'motor',\n 'sensor',\n 'camera',\n 'battery',\n // 'memory', ??\n]\n// Your code\nequal(components[0].toLowerCase(), 'motor')\n" + }, + { + "description": "components second element sensor", + "code": "\n\nconst replaceComponents = ['sensor', 'battery', 'motor', 'brain']\nlet swapComponents = ['motor', 'battery']\nlet robotParts = [\n 'motor',\n 'sensor',\n 'camera',\n 'battery',\n // 'memory', ??\n]\n// Your code\nequal(components[1].toLowerCase(), 'sensor')\n" + }, + { + "description": "components third element battery", + "code": "\n\nconst replaceComponents = ['sensor', 'battery', 'motor', 'brain']\nlet swapComponents = ['motor', 'battery']\nlet robotParts = [\n 'motor',\n 'sensor',\n 'camera',\n 'battery',\n // 'memory', ??\n]\n// Your code\nequal(components[2].toLowerCase(), 'battery')\n" + }, + { + "description": "components fourth element camera", + "code": "\n\nconst replaceComponents = ['sensor', 'battery', 'motor', 'brain']\nlet swapComponents = ['motor', 'battery']\nlet robotParts = [\n 'motor',\n 'sensor',\n 'camera',\n 'battery',\n // 'memory', ??\n]\n// Your code\nequal(components[3].toLowerCase(), 'camera')\n" + }, + { + "description": "components we must not have a fifth element", + "code": "\n\nconst replaceComponents = ['sensor', 'battery', 'motor', 'brain']\nlet swapComponents = ['motor', 'battery']\nlet robotParts = [\n 'motor',\n 'sensor',\n 'camera',\n 'battery',\n // 'memory', ??\n]\n// Your code\nequal(components[4], undefined)\n" + }, + { + "description": "firstPart is the value of the first element", + "code": "\n\nconst replaceComponents = ['sensor', 'battery', 'motor', 'brain']\nlet robotParts = [\n 'motor',\n 'sensor',\n 'battery',\n 'camera',\n // 'memory', ??\n]\nconst swapComponents = ['sensor', 'battery', 'motor']\n\n// Your code\n\nequal(firstPart, 'motor')\n" + }, + { + "description": "firstPart is the value of the first element even if we change the list", + "code": "\n\nconst replaceComponents = ['sensor', 'battery', 'motor', 'brain']\nlet robotParts = [\n 'sensor',\n 'motor',\n 'camera',\n 'battery',\n // 'memory', ??\n]\nconst swapComponents = ['sensor', 'battery', 'motor']\n\n// Your code\n\nequal(firstPart, 'sensor')\n" + }, + { + "description": "lastPart is the value of the last element", + "code": "\n\nconst replaceComponents = ['sensor', 'battery', 'motor', 'brain']\nlet robotParts = [\n 'motor',\n 'sensor',\n 'battery',\n 'camera',\n // 'memory', ??\n]\nconst swapComponents = ['sensor', 'battery', 'motor']\n\n// Your code\n\nequal(lastPart, 'camera')\n" + }, + { + "description": "lastPart is the value of the last element even if we change the list", + "code": "\n\nconst replaceComponents = ['sensor', 'battery', 'motor', 'brain']\nlet robotParts = [\n 'sensor',\n 'motor',\n 'camera',\n 'battery',\n // 'memory', ??\n]\nconst swapComponents = ['sensor', 'battery', 'motor']\n\n// Your code\n\nequal(lastPart, 'battery')\n" + }, + { + "description": "comboParts is an array of lastPart and firstPart", + "code": "\n\nconst replaceComponents = ['sensor', 'battery', 'motor', 'brain']\nlet robotParts = [\n 'motor',\n 'sensor',\n 'battery',\n 'camera',\n // 'memory', ??\n]\nconst swapComponents = ['sensor', 'battery', 'motor']\n\n// Your code\n\nequal(comboParts, ['camera', 'motor'])\n" + }, + { + "description": "comboParts is an array of lastPart and firstPart even if we change the list", + "code": "\n\nconst replaceComponents = ['sensor', 'battery', 'motor', 'brain']\nlet robotParts = [\n 'sensor',\n 'motor',\n 'camera',\n 'battery',\n // 'memory', ??\n]\nconst swapComponents = ['sensor', 'battery', 'motor']\n\n// Your code\n\nequal(comboParts, ['battery', 'sensor'])\n" + }, + { + "description": "replaceComponents third element is 'enhanced'", + "code": "\n\nlet robotParts = [\n 'motor',\n 'sensor',\n 'camera',\n 'battery',\n // 'memory', ??\n]\nconst swapComponents = ['sensor', 'battery', 'motor']\nconst replaceComponents = ['sensor', 'battery', 'motor', 'brain']\n\n// Your code\n\nequal(replaceComponents, ['sensor', 'battery', 'enhanced', 'brain'])\n" + }, + { + "description": "1st and 2nd elements of swapComponents are swapped pif,paf,pom", + "code": "\n\nconst replaceComponents = ['sensor', 'battery', 'motor', 'brain']\nlet robotParts = [\n 'motor',\n 'sensor',\n 'camera',\n 'battery',\n // 'memory', ??\n]\nlet swapComponents = ['sensor', 'battery', 'motor']\n\n// Your code\n\nequal(swapComponents, ['battery', 'sensor', 'motor'])\n" + } +] diff --git a/test-images/test-js/tests/objects-around.json b/test-images/test-js/tests/objects-around.json new file mode 100644 index 0000000..09e390b --- /dev/null +++ b/test-images/test-js/tests/objects-around.json @@ -0,0 +1,26 @@ +[ + { + "description": "variable myRobot is declared and of type object", + "code": "let robot = {\n name: 'Freddy',\n age: 27,\n hasEnergy: false,\n}\n\n// Your code\n\nequal(typeof myRobot, 'object')" + }, + { + "description": "property name from myRobot is of type string", + "code": "let robot = {\n name: 'Freddy',\n age: 27,\n hasEnergy: false,\n}\n\n// Your code\n\nequal(typeof myRobot.name, 'string')" + }, + { + "description": "property age from myRobot is of type number", + "code": "let robot = {\n name: 'Freddy',\n age: 27,\n hasEnergy: false,\n}\n\n// Your code\n\nequal(typeof myRobot.age, 'number')" + }, + { + "description": "property hasEnergy from myRobot is of type boolean", + "code": "let robot = {\n name: 'Freddy',\n age: 27,\n hasEnergy: false,\n}\n\n// Your code\n\nequal(typeof myRobot.hasEnergy, 'boolean')" + }, + { + "description": "all 3 variable should be defined and have the right values", + "code": "let robot = {\n name: 'Freddy',\n age: 27,\n hasEnergy: false,\n}\n\n// Your code\n\nequal({ name, age, hasEnergy }, robot)" + }, + { + "description": "value should also work for Jean-Pierre", + "code": "let robot = {\n name: 'Jean-Pierre',\n age: 65,\n hasEnergy: true,\n}\n\n// Your code\n\nequal({ name, age, hasEnergy }, robot)" + } +] diff --git a/test-images/test-js/tests/only-if.json b/test-images/test-js/tests/only-if.json new file mode 100644 index 0000000..457f8e3 --- /dev/null +++ b/test-images/test-js/tests/only-if.json @@ -0,0 +1,82 @@ +[ + { + "description": "Test with the falsy value 0", + "code": "const args = saveArguments(console, 'log')\nlet truth = 0\nlet user = { activeMembership: true, age: 22 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket\nlet ticketSold = 3\n\n// Your code\n\nequal(args[0]?.[0], 'Lies !!!!')" + }, + { + "description": "Test with the falsy value NaN", + "code": "const args = saveArguments(console, 'log')\nlet truth = NaN\nlet user = { activeMembership: true, age: 22 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket\nlet ticketSold = 3\n\n// Your code\n\nequal(args[0]?.[0], 'Lies !!!!')" + }, + { + "description": "Test with the falsy value undefined", + "code": "const args = saveArguments(console, 'log')\nlet truth = undefined\nlet user = { activeMembership: true, age: 22 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket\nlet ticketSold = 3\n\n// Your code\n\nequal(args[0]?.[0], 'Lies !!!!')" + }, + { + "description": "Test with the falsy value null", + "code": "const args = saveArguments(console, 'log')\nlet truth = null\nlet user = { activeMembership: true, age: 22 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket\nlet ticketSold = 3\n\n// Your code\n\nequal(args[0]?.[0], 'Lies !!!!')" + }, + { + "description": "Test with the falsy value ''", + "code": "const args = saveArguments(console, 'log')\nlet truth = ''\nlet user = { activeMembership: true, age: 22 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket\nlet ticketSold = 3\n\n// Your code\n\nequal(args[0]?.[0], 'Lies !!!!')" + }, + { + "description": "Test with the falsy value false", + "code": "const args = saveArguments(console, 'log')\nlet truth = false\nlet user = { activeMembership: true, age: 22 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket\nlet ticketSold = 3\n\n// Your code\n\nequal(args[0]?.[0], 'Lies !!!!')" + }, + { + "description": "Test with the truthy value 'Sure'", + "code": "const args = saveArguments(console, 'log')\nlet truth = 'Sure'\nlet user = { activeMembership: true, age: 22 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket\nlet ticketSold = 3\n\n// Your code\n\nequal(args[0]?.[0], 'The truth was spoken.')" + }, + { + "description": "Test with the truthy value []", + "code": "const args = saveArguments(console, 'log')\nlet truth = []\nlet user = { activeMembership: true, age: 22 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket\nlet ticketSold = 3\n\n// Your code\n\nequal(args[0]?.[0], 'The truth was spoken.')" + }, + { + "description": "Test with the truthy value {}", + "code": "const args = saveArguments(console, 'log')\nlet truth = {}\nlet user = { activeMembership: true, age: 22 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket\nlet ticketSold = 3\n\n// Your code\n\nequal(args[0]?.[0], 'The truth was spoken.')" + }, + { + "description": "Test with the truthy value true", + "code": "const args = saveArguments(console, 'log')\nlet truth = true\nlet user = { activeMembership: true, age: 22 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket\nlet ticketSold = 3\n\n// Your code\n\nequal(args[0]?.[0], 'The truth was spoken.')" + }, + { + "description": "Test with the truthy value -0.1", + "code": "const args = saveArguments(console, 'log')\nlet truth = -0.1\nlet user = { activeMembership: true, age: 22 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket\nlet ticketSold = 3\n\n// Your code\n\nequal(args[0]?.[0], 'The truth was spoken.')" + }, + { + "description": "Test with a user that can have the promotion", + "code": "const args = saveArguments(console, 'log')\nlet truth = 1\nlet user = { activeMembership: true, age: 22 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket\nlet ticketSold = 3\n\n// Your code\n\nequal(ticket, 'You can benefit from our special promotion')" + }, + { + "description": "Test with a user that is too old", + "code": "const args = saveArguments(console, 'log')\nlet truth = 1\nlet user = { activeMembership: true, age: 33 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket = 'You can benefit from our special promotion'\nlet ticketSold = 3\n\n// Your code\n\nequal(ticket, 'You cannot benefit from our special promotion')" + }, + { + "description": "Test with a user that is too young", + "code": "const args = saveArguments(console, 'log')\nlet truth = 1\nlet user = { activeMembership: true, age: 12 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket = 'You can benefit from our special promotion'\nlet ticketSold = 3\n\n// Your code\n\nequal(ticket, 'You cannot benefit from our special promotion')" + }, + { + "description": "Test with a user that doesn't have an active membership", + "code": "const args = saveArguments(console, 'log')\nlet truth = 1\nlet user = { activeMembership: false, age: 21 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket = 'You can benefit from our special promotion'\nlet ticketSold = 3\n\n// Your code\n\nequal(ticket, 'You cannot benefit from our special promotion')" + }, + { + "description": "Test with a user that can have the promotion but is just at the limit", + "code": "const args = saveArguments(console, 'log')\nlet truth = 1\nlet user = { activeMembership: true, age: 25 }\nlet customer = { cash: 20, hasVoucher: false }\nlet ticket = 'You can benefit from our special promotion'\nlet ticketSold = 3\n\n// Your code\n\nequal(ticket, 'You can benefit from our special promotion')" + }, + { + "description": "Test with a customer that has enough cash", + "code": "let truth = 0\nlet ticketSold = 8\nlet customer = { cash: 20, hasVoucher: false }\nlet user = { activeMembership: true, age: 22 }\nlet ticket\n\n// Your code\n\nequal(ticketSold, 9)" + }, + { + "description": "Test with a customer that has a voucher", + "code": "let truth = 0\nlet ticketSold = 5\nlet customer = { cash: 0, hasVoucher: true }\nlet user = { activeMembership: true, age: 22 }\nlet ticket\n\n// Your code\n\nequal(ticketSold, 6)" + }, + { + "description": "Test with a customer that has a voucher and cash", + "code": "let truth = 0\nlet ticketSold = 6\nlet customer = { cash: 42, hasVoucher: true }\nlet user = { activeMembership: true, age: 22 }\nlet ticket\n\n// Your code\n\nequal(ticketSold, 7)" + }, + { + "description": "Test with a customer that can not afford the ticket", + "code": "let truth = 0\nlet ticketSold = 3\nlet customer = { cash: 3, hasVoucher: false }\nlet user = { activeMembership: true, age: 22 }\nlet ticket\n\n// Your code\n\nequal(ticketSold, 3)" + } +] diff --git a/test-images/test-js/tests/play-with-variables.json b/test-images/test-js/tests/play-with-variables.json new file mode 100644 index 0000000..a15ebf9 --- /dev/null +++ b/test-images/test-js/tests/play-with-variables.json @@ -0,0 +1,22 @@ +[ + { + "description": "escapeFromDelimiters is declared and includes a double-quote", + "code": "let power = 'under 9000'\n\n// Your code\nif (typeof escapeFromDelimiters === 'undefined') {\n throw Error(\n `You didn't even define the variable... we've been through this already !`,\n )\n}\n\nif (!escapeFromDelimiters.includes('\"')) {\n throw Error('escapeFromDelimiters must include a double-quote\"')\n}" + }, + { + "description": "escapeFromDelimiters includes a single-quote", + "code": "let power = 'under 9000'\n\n// Your code\nif (!escapeFromDelimiters.includes(\"'\")) {\n throw Error(\"escapeFromDelimiters must include a single-quote'\")\n}" + }, + { + "description": "escapeFromDelimiters includes a backtick", + "code": "let power = 'under 9000'\n\n// Your code\nif (!escapeFromDelimiters.includes('`')) {\n throw Error('escapeFromDelimiters must include a backtick `')\n}" + }, + { + "description": "escapeTheEscape includes a backslash", + "code": "let power = 'under 9000'\n\n// Your code\nif (!new TextEncoder().encode(escapeTheEscape).includes(92)) {\n throw Error('escapeTheEscape must includes a backslash')\n}" + }, + { + "description": "The value of power must have changed", + "code": "let power = 'under 9000'\n\n// Your code\n\nequal(power, 'levelMax')" + } +] diff --git a/test-images/test-js/tests/star-forge.json b/test-images/test-js/tests/star-forge.json new file mode 100644 index 0000000..a58a636 --- /dev/null +++ b/test-images/test-js/tests/star-forge.json @@ -0,0 +1,6 @@ +[ + { + "description": "Star-Forge", + "code": "// Your code\n\nequal(ready, 'Yes')" + } +] diff --git a/test-images/test-js/tests/the-smooth-operator.json b/test-images/test-js/tests/the-smooth-operator.json new file mode 100644 index 0000000..3ac6bb5 --- /dev/null +++ b/test-images/test-js/tests/the-smooth-operator.json @@ -0,0 +1,26 @@ +[ + { + "description": "values of the variable are a result of the operations on the variable smooth ( 10 )", + "code": "let name = 'blank'\nlet age = 0\nlet smooth = 10\n\n// Your code\n\nequal(lessSmooth, 9)\nequal(semiSmooth, 5)\nequal(plus11, 21)" + }, + { + "description": "values of the variable are a result of the operations on the variable smooth ( 27 )", + "code": "let name = 'blank'\nlet age = 0\nlet smooth = 27\n\n// Your code\nequal(lessSmooth, 26)\nequal(semiSmooth, 13.5)\nequal(plus11, 38)" + }, + { + "description": "ultraSmooth should be the square of the value of smooth ( 10 )", + "code": "let name = 'blank'\nlet age = 0\nlet smooth = 10\n\n// Your code\n\nequal(ultraSmooth, 100)" + }, + { + "description": "ultraSmooth should be the square of the value of smooth ( 27 )", + "code": "let name = 'blank'\nlet age = 0\nlet smooth = 27\n\n// Your code\n\nequal(ultraSmooth, 729)" + }, + { + "description": "presentation value includes age and name .", + "code": "let name = 'Patrick'\nlet age = 48\nlet smooth = 0\n\n// Your code\nequal(presentation, `Hello, my name is Patrick and I'm 48 years old`)" + }, + { + "description": "presentation value still includes age and name .", + "code": "let smooth = 0\nlet name = 'Jeremy'\nlet age = 27\n\n// Your code\nequal(presentation, `Hello, my name is Jeremy and I'm 27 years old`)" + } +] diff --git a/test-images/test-js/tests/transform-objects.json b/test-images/test-js/tests/transform-objects.json new file mode 100644 index 0000000..56d80c8 --- /dev/null +++ b/test-images/test-js/tests/transform-objects.json @@ -0,0 +1,22 @@ +[ + { + "description": "duplicate value should repeat 'I told you so'", + "code": "let robot = {}\nlet sentence = 'I told you so'\n// Your code\nequal(duplicate, 'I told you so, I told you so!')" + }, + { + "description": "duplicate value should repeat 'Not again'", + "code": "let robot = {}\nlet sentence = 'Not again'\n// Your code\nequal(duplicate, 'Not again, Not again!')" + }, + { + "description": "duplicate value should repeat 'I knew it'", + "code": "let robot = {}\nlet sentence = 'I knew it'\n// Your code\nequal(duplicate, 'I knew it, I knew it!')" + }, + { + "description": "Altered object must match the expected result Nova", + "code": "let sentence = ''\nlet robot = {\n brand: 'Nova',\n batteryLevel: 247,\n}\n\n// Your code\n\nequal(robot, {\n brand: 'Nova',\n model: 'RX-78',\n batteryLevel: 257,\n fullName: 'Nova RX-78',\n})" + }, + { + "description": "Altered object must match the expected result Ignite", + "code": "let sentence = ''\nlet robot = {\n brand: 'Ignite',\n batteryLevel: 123,\n}\n\n// Your code\n\nequal(robot, {\n brand: 'Ignite',\n model: 'RX-78',\n batteryLevel: 133,\n fullName: 'Ignite RX-78',\n})" + } +]