From 6960fcc79bd4d528598399ee03fd1ade872b9f02 Mon Sep 17 00:00:00 2001 From: Catalin Constantin Mititiuc Date: Mon, 16 Jun 2025 22:41:31 -0700 Subject: [PATCH] Add integration test-helpers and refactor integration tests --- jest.config.integ.cjs | 5 +- package.json | 2 +- public/index.html | 4 +- .../{scenario-test.svg => scenario.svg} | 8 +- test/integration/helpers.cjs | 72 +++++++ test/integration/select.test.js | 182 ++++++++---------- 6 files changed, 161 insertions(+), 112 deletions(-) rename test/integration/fixtures/{scenario-test.svg => scenario.svg} (64%) diff --git a/jest.config.integ.cjs b/jest.config.integ.cjs index 28562ea..083cbd7 100644 --- a/jest.config.integ.cjs +++ b/jest.config.integ.cjs @@ -5,8 +5,9 @@ module.exports = { globalTeardown: './test/integration/teardown.cjs', setupFiles: ['./test/integration/helpers.cjs'], testPathIgnorePatterns: ['/node_modules/', 'test/unit'], - testTimeout: 5000, + verbose: true, + randomize: true, globals: { testServerUrl: 'http://localhost:3005/' - } + }, }; diff --git a/package.json b/package.json index 620fc02..9c26357 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "scripts": { "start": "node server.cjs", "start:esbuildserver": "node esbuild-server.mjs", - "test:integ": "node --trace-warnings ./node_modules/.bin/jest --detectOpenHandles --config jest.config.integ.cjs", + "test:integ": "node ./node_modules/.bin/jest --detectOpenHandles --config jest.config.integ.cjs", "test:integ:debug": "NODE_INSPECT_RESUME_ON_START=1 node inspect ./node_modules/jest/bin/jest.js --config jest.config.integ.cjs --runInBand", "test": "jest" } diff --git a/public/index.html b/public/index.html index df3fc65..b78df5b 100644 --- a/public/index.html +++ b/public/index.html @@ -145,7 +145,7 @@ Loading... - +
Distance: - @@ -246,7 +246,7 @@ diff --git a/test/integration/fixtures/scenario-test.svg b/test/integration/fixtures/scenario.svg similarity index 64% rename from test/integration/fixtures/scenario-test.svg rename to test/integration/fixtures/scenario.svg index 77afd6b..e5b7121 100644 --- a/test/integration/fixtures/scenario-test.svg +++ b/test/integration/fixtures/scenario.svg @@ -9,13 +9,7 @@ - - - - - - - + diff --git a/test/integration/helpers.cjs b/test/integration/helpers.cjs index 7e3e0fd..d38f1b6 100644 --- a/test/integration/helpers.cjs +++ b/test/integration/helpers.cjs @@ -1,5 +1,8 @@ const { mkdir, writeFile } = require('node:fs/promises') , path = require('path') + , { By, until } = require('selenium-webdriver') + , { JSDOM } = require('jsdom') + , { HttpResponse } = require('selenium-webdriver/devtools/networkinterceptor') ; global.takeScreenshot = async (driver) => { @@ -14,3 +17,72 @@ global.url = (relativeOrAbsolute) => { const location = new URL(relativeOrAbsolute, global.testServerUrl); return location.href; } + +global.page = { + waitUntilMapLoaded: async function (driver) { + const mapPlaceholderEl = await driver.findElement(By.css('.map-placeholder')); + await driver.wait(until.elementIsNotVisible(mapPlaceholderEl), 1000); + }, + + findElInFrame: async function (frame, locator) { + const driver = frame.getDriver(); + await driver.switchTo().frame(frame); + return driver.findElement(locator); + }, + + expectSelected: async function (el) { + expect(await el.getAttribute('class')).toEqual(expect.stringContaining('selected')); + }, + + expectNotSelected: async function (el) { + expect(await el.getAttribute('class')).toEqual(expect.not.stringContaining('selected')); + }, + + troopCounterSelector: function ({ allegiance = 'attacker', troopNumber = 1 } = {}) { + return `.counter[data-allegiance="${allegiance}"][data-number="${troopNumber}"]`; + }, + + findMapResourceEl: function (driver) { + return driver.findElement(By.css('object')); + }, + + findTroopRecordEl: function (driver) { + return driver.findElement(By.css('.soldier-record')); + } +}; + +global.mockResponse = async function mockResponse(driver, urlPath, template, createFn) { + const connection = driver.createCDPConnection('page'); + const dom = new JSDOM(template); + const contents = createFn(dom.window.document); + + const httpResponse = new HttpResponse(url(urlPath)); + httpResponse.body = contents; + httpResponse.addHeaders('Content-Type', 'image/svg+xml'); + + await driver.onIntercept(await connection, httpResponse, async () => {}); +} + +global.createTroopCounter = function ( + allegiance = 'attacker', + troopNumber = 1, + squadNumber = 1, + weapon = 'rifle' +) { + return JSDOM.fragment(` + + + + + + `); +} + +global.placeCounter = function (document, counter, { x, y }) { + document.querySelector(`g[data-y="${y}"] g[data-x="${x}"]`).append(counter); +} + +global.selectCounter = function (counter) { + counter.querySelector('.counter').classList.add('selected'); + return counter; +} diff --git a/test/integration/select.test.js b/test/integration/select.test.js index d9b632e..acde0f0 100644 --- a/test/integration/select.test.js +++ b/test/integration/select.test.js @@ -1,136 +1,118 @@ -const { Builder, By, until } = require('selenium-webdriver'), +const { Builder, By } = require('selenium-webdriver'), chrome = require('selenium-webdriver/chrome.js'), chromeOptions = new chrome.Options(), - { readFile, readdir } = require('node:fs/promises'), - { HttpResponse } = require('selenium-webdriver/devtools/networkinterceptor'); + { readFile, readdir } = require('node:fs/promises'); chromeOptions.addArguments('--headless', '--disable-gpu', '--no-sandbox'); const buildPath = 'build/assets/images'; const defaultScenario = 'scenario-side_show'; -const fixtureFilePath = './test/integration/fixtures/scenario-test.svg'; +const fixtureFilePath = './test/integration/fixtures/scenario.svg'; -const selected = expect.stringContaining('selected'); -const notSelected = expect.not.stringContaining('selected'); - -let driver, httpResponse; +let driver, fixture, scenario; beforeAll(async () => { - const filenames = await readdir(buildPath); - const scenario = filenames.find(filename => filename.includes(defaultScenario)); - - httpResponse = new HttpResponse(url(`/assets/images/${scenario}`)); - httpResponse.body = await readFile(fixtureFilePath, 'utf8'); - httpResponse.addHeaders('Content-Type', 'image/svg+xml'); + const dirread = readdir(buildPath); + const fileRead = readFile(fixtureFilePath, 'utf8'); + const filenames = await dirread; + scenario = filenames.find(filename => filename.includes(defaultScenario)); + fixture = await fileRead; }); beforeEach(async () => { driver = new Builder().forBrowser('chrome').setChromeOptions(chromeOptions).build(); - const connection = await driver.createCDPConnection('page') - await driver.onIntercept(connection, httpResponse, async () => {}); - await driver.get(url('/')); - const mapPlaceholderEl = await driver.findElement(By.css('.map-placeholder')); - await driver.wait(until.elementIsNotVisible(mapPlaceholderEl), 1000); }); -it('selects and deselects a trooper by clicking on its counter', async () => { - const record = await driver.findElement(By.css('.soldier-record')); - const objectEl = await driver.findElement(By.css('object')); - const selector = '.counter[data-allegiance="attacker"][data-number="1"]'; +describe('a trooper', () => { + beforeEach(async () => { + await mockResponse(driver, `/assets/images/${scenario}`, fixture, (document) => { + placeCounter(document, createTroopCounter(), { x: 1, y: 1 }); + return `\n` + document.querySelector('svg').outerHTML; + }); + }); - expect(await record.getAttribute('class')).toEqual(notSelected); - await driver.switchTo().frame(objectEl); - const svg = await driver.findElement(By.css('svg')); - const counter = await driver.findElement(By.css(selector), svg); - expect(await counter.getAttribute('class')).toEqual(notSelected); + it('is selected by clicking on its counter', async () => { + await driver.get(url('/')); + await page.waitUntilMapLoaded(driver); - await counter.click(); + const record = page.findTroopRecordEl(driver); + const mapResource = page.findMapResourceEl(driver); - expect(await counter.getAttribute('class')).toEqual(selected); - await driver.switchTo().defaultContent(); - expect(await record.getAttribute('class')).toEqual(selected); + await page.expectNotSelected(await record); + const counter = await page.findElInFrame(await mapResource, By.css(page.troopCounterSelector())); + await page.expectNotSelected(counter); - await driver.switchTo().frame(objectEl); - await counter.click(); + await counter.click(); - expect(await counter.getAttribute('class')).toEqual(notSelected); - await driver.switchTo().defaultContent(); - expect(await record.getAttribute('class')).toEqual(notSelected); + await page.expectSelected(counter); + await driver.switchTo().defaultContent(); + await page.expectSelected(await record); + }); + + it('is selected by clicking on its record', async () => { + await driver.get(url('/')); + await page.waitUntilMapLoaded(driver); + + const record = page.findTroopRecordEl(driver); + const mapResource = page.findMapResourceEl(driver); + + await page.expectNotSelected(await record); + const counter = await page.findElInFrame(await mapResource, By.css(page.troopCounterSelector())); + await page.expectNotSelected(counter); + + await driver.switchTo().defaultContent(); + await record.click(); + + await page.expectSelected(await record); + await driver.switchTo().frame(await mapResource); + await page.expectSelected(counter); + }); }); -it('selects and deselects trooper by clicking on its record', async () => { - const record = await driver.findElement(By.css('.soldier-record')); - const objectEl = await driver.findElement(By.css('object')); - const selector = '.counter[data-allegiance="attacker"][data-number="1"]'; +describe('a selected trooper', () => { + beforeEach(async () => { + await mockResponse(driver, `/assets/images/${scenario}`, fixture, (document) => { + placeCounter(document, selectCounter(createTroopCounter()), { x: 1, y: 1 }); + return `\n` + document.querySelector('svg').outerHTML; + }); + }); - expect(await record.getAttribute('class')).toEqual(notSelected); - await driver.switchTo().frame(objectEl); - const svg = await driver.findElement(By.css('svg')); - const counter = await driver.findElement(By.css(selector), svg); - expect(await counter.getAttribute('class')).toEqual(notSelected); + it('is deselected by clicking on its counter', async () => { + await driver.get(url('/')); + await page.waitUntilMapLoaded(driver); - await driver.switchTo().defaultContent(); - await record.click(); + const record = page.findTroopRecordEl(driver); + const mapResource = page.findMapResourceEl(driver); - expect(await record.getAttribute('class')).toEqual(selected); - await driver.switchTo().frame(objectEl); - expect(await counter.getAttribute('class')).toEqual(selected); + await page.expectSelected(await record); + const counter = await page.findElInFrame(await mapResource, By.css(page.troopCounterSelector())); + await page.expectSelected(counter); - await driver.switchTo().defaultContent(); - await record.click(); + await counter.click(); - expect(await record.getAttribute('class')).toEqual(notSelected); - await driver.switchTo().frame(objectEl); - expect(await counter.getAttribute('class')).toEqual(notSelected); -}); + await page.expectNotSelected(counter); + await driver.switchTo().defaultContent(); + await page.expectNotSelected(await record); + }); -it('selects a trooper by clicking on its counter and deselects it by clicking on its record', async () => { - const record = await driver.findElement(By.css('.soldier-record')); - const objectEl = await driver.findElement(By.css('object')); - const selector = '.counter[data-allegiance="attacker"][data-number="1"]'; + it('is deselected by clicking on its record', async () => { + await driver.get(url('/')); + await page.waitUntilMapLoaded(driver); - expect(await record.getAttribute('class')).toEqual(notSelected); - await driver.switchTo().frame(objectEl); - const svg = await driver.findElement(By.css('svg')); - const counter = await driver.findElement(By.css(selector), svg); - expect(await counter.getAttribute('class')).toEqual(notSelected); + const record = page.findTroopRecordEl(driver); + const mapResource = page.findMapResourceEl(driver); - await counter.click(); + await page.expectSelected(await record); + const counter = await page.findElInFrame(await mapResource, By.css(page.troopCounterSelector())); + await page.expectSelected(counter); - expect(await counter.getAttribute('class')).toEqual(selected); - await driver.switchTo().defaultContent(); - expect(await record.getAttribute('class')).toEqual(selected); + await driver.switchTo().defaultContent(); + await record.click(); - await record.click(); - - expect(await record.getAttribute('class')).toEqual(notSelected); - await driver.switchTo().frame(objectEl); - expect(await counter.getAttribute('class')).toEqual(notSelected); -}); - -it('selects a trooper by clicking on its record and deselects it by clicking on its counter', async () => { - const record = await driver.findElement(By.css('.soldier-record')); - const objectEl = await driver.findElement(By.css('object')); - const selector = '.counter[data-allegiance="attacker"][data-number="1"]'; - - expect(await record.getAttribute('class')).toEqual(notSelected); - await driver.switchTo().frame(objectEl); - const svg = await driver.findElement(By.css('svg')); - const counter = await driver.findElement(By.css(selector), svg); - expect(await counter.getAttribute('class')).toEqual(notSelected); - - await driver.switchTo().defaultContent(); - await record.click(); - - expect(await record.getAttribute('class')).toEqual(selected); - await driver.switchTo().frame(objectEl); - expect(await counter.getAttribute('class')).toEqual(selected); - - await counter.click(); - - expect(await counter.getAttribute('class')).toEqual(notSelected); - await driver.switchTo().defaultContent(); - expect(await record.getAttribute('class')).toEqual(notSelected); + await page.expectNotSelected(await record); + await driver.switchTo().frame(await mapResource); + await page.expectNotSelected(counter); + }); }); afterEach(async () => {