From b55f7a9364ac32cf4de3363f8b977ad92429c97e Mon Sep 17 00:00:00 2001 From: Kyle Florence Date: Thu, 8 Feb 2024 16:33:39 -0600 Subject: [PATCH] Improve puzzle 011, fix a few bugs (#17) Updated the puzzle to include a non-directional portal to further illustrate the rules of portals. The portal mask has been updated to select the tile the mask applies to before applying the mask (which ensures the toolbar for that tile is selected). In addition, the before/after edit actions on tiles have been updated to disable all modifiers when a mask is active on that tile. The use of modifiers while a mask was active was causing bugs, especially in the case of a portal. This also includes a bug fix for updating beam references in a tile. Previously, all beam references were removed from a tile when a step for that beam was removed from a tile. However, sometimes there are multiple steps for a single beam in a tile, so the beam reference only needs to be removed if there are no longer any steps in that tile for the beam. Additionally, functional tests have been updated to log to console. A bug relating to wait conditions was also fixed, as was a bug found in puzzle 008 related to the filter item. --- .github/workflows/pull-request.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- package-lock.json | 18 +++++++-------- package.json | 4 ++-- src/components/items/beam.js | 6 ++--- src/components/items/filter.js | 2 +- src/components/items/portal.js | 1 + src/components/items/tile.js | 2 ++ src/components/modifier.js | 15 ++++++++---- src/components/puzzle.js | 4 ++++ src/components/state.js | 1 + src/puzzles/011.js | 10 ++++---- test/fixtures.js | 37 ++++++++++++++++++++++++------ test/puzzles/011.js | 12 ++++++---- 14 files changed, 79 insertions(+), 41 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index c355086..7d305d9 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -8,9 +8,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v4 - name: Setup Node - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 98baaad..41279aa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,9 +8,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v4 - name: Setup Node - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies diff --git a/package-lock.json b/package-lock.json index 1b52b9e..5db80d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "paper": "^0.12.17" }, "devDependencies": { - "chromedriver": "^120.0.1", + "chromedriver": "^121.0.0", "mocha": "^10.2.0", "parcel": "^2.9.3", "selenium-webdriver": "^4.16.0", @@ -2815,14 +2815,14 @@ } }, "node_modules/chromedriver": { - "version": "120.0.1", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-120.0.1.tgz", - "integrity": "sha512-ETTJlkibcAmvoKsaEoq2TFqEsJw18N0O9gOQZX6Uv/XoEiOV8p+IZdidMeIRYELWJIgCZESvlOx5d1QVnB4v0w==", + "version": "121.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-121.0.0.tgz", + "integrity": "sha512-ZIKEdZrQAfuzT/RRofjl8/EZR99ghbdBXNTOcgJMKGP6N/UL6lHUX4n6ONWBV18pDvDFfQJ0x58h5AdOaXIOMw==", "dev": true, "hasInstallScript": true, "dependencies": { "@testim/chrome-version": "^1.1.4", - "axios": "^1.6.0", + "axios": "^1.6.5", "compare-versions": "^6.1.0", "extract-zip": "^2.0.1", "https-proxy-agent": "^5.0.1", @@ -9301,13 +9301,13 @@ "dev": true }, "chromedriver": { - "version": "120.0.1", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-120.0.1.tgz", - "integrity": "sha512-ETTJlkibcAmvoKsaEoq2TFqEsJw18N0O9gOQZX6Uv/XoEiOV8p+IZdidMeIRYELWJIgCZESvlOx5d1QVnB4v0w==", + "version": "121.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-121.0.0.tgz", + "integrity": "sha512-ZIKEdZrQAfuzT/RRofjl8/EZR99ghbdBXNTOcgJMKGP6N/UL6lHUX4n6ONWBV18pDvDFfQJ0x58h5AdOaXIOMw==", "dev": true, "requires": { "@testim/chrome-version": "^1.1.4", - "axios": "^1.6.0", + "axios": "^1.6.5", "compare-versions": "^6.1.0", "extract-zip": "^2.0.1", "https-proxy-agent": "^5.0.1", diff --git a/package.json b/package.json index df43aec..cac5de8 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,11 @@ "build": "parcel build --public-url 'https://kflorence.github.io/beaming'", "start": "parcel", "test": "npm run test-lint && npm run test-functional", - "test-functional": "mocha test --recursive --timeout 10000", + "test-functional": "mocha test --recursive --timeout 20000", "test-lint": "standard" }, "devDependencies": { - "chromedriver": "^120.0.1", + "chromedriver": "^121.0.0", "mocha": "^10.2.0", "parcel": "^2.9.3", "selenium-webdriver": "^4.16.0", diff --git a/src/components/items/beam.js b/src/components/items/beam.js index 1eccfc8..38b1ae9 100644 --- a/src/components/items/beam.js +++ b/src/components/items/beam.js @@ -96,7 +96,7 @@ export class Beam extends Item { step.index = this.#stepIndex = this.#steps.length - 1 - if (!step.tile.items.some((item) => item === this)) { + if (!step.tile.items.some((item) => item.equals(this))) { // Add this beam to the tile item list so other beams can see it step.tile.addItem(this) } @@ -681,9 +681,9 @@ export class Beam extends Item { console.debug(this.toString(), 'removed steps: ', deletedSteps) - // Remove beam from tiles it is being removed from const tiles = [...new Set(deletedSteps.map((step) => step.tile))] - tiles.forEach((tile) => tile.removeItem(this)) + // Remove references to the beam in any tiles it is no longer in + tiles.filter((tile) => this.getSteps(tile).length === 0).forEach((tile) => tile.removeItem(this)) deletedSteps.forEach((step) => step.onRemove(step)) diff --git a/src/components/items/filter.js b/src/components/items/filter.js index ecc6b42..aee8430 100644 --- a/src/components/items/filter.js +++ b/src/components/items/filter.js @@ -34,7 +34,7 @@ export class Filter extends movable(Item) { return [getColorElement(this.color)] } - onCollision (beam, puzzle, { currentStep, nextStep }) { + onCollision ({ currentStep, nextStep }) { // The beam will collide with the filter twice, on entry and exit, so ignore the first one, but track in state return nextStep.copy( currentStep.state.has(StepState.Filter) diff --git a/src/components/items/portal.js b/src/components/items/portal.js index a4b596f..e41e759 100644 --- a/src/components/items/portal.js +++ b/src/components/items/portal.js @@ -161,6 +161,7 @@ export class Portal extends movable(rotatable(Item)) { } ) + puzzle.updateSelectedTile(currentStep.tile) puzzle.mask(mask) // This will cause the beam to stop diff --git a/src/components/items/tile.js b/src/components/items/tile.js index 189fd04..76aa888 100644 --- a/src/components/items/tile.js +++ b/src/components/items/tile.js @@ -46,11 +46,13 @@ export class Tile extends Item { afterModify () { this.setStyle(this.selected ? 'selected' : 'default') + this.modifiers.forEach((modifier) => modifier.update({ disabled: false })) } beforeModify () { this.group.bringToFront() this.setStyle('edit') + this.modifiers.forEach((modifier) => modifier.update({ disabled: true })) } getState () { diff --git a/src/components/modifier.js b/src/components/modifier.js index 371d87a..e390c1c 100644 --- a/src/components/modifier.js +++ b/src/components/modifier.js @@ -185,15 +185,20 @@ export class Modifier extends Stateful { options || {} ) - this.disabled = options.disabled + if (!this.immutable) { + this.disabled = options.disabled + } + this.name = options.name this.title = options.title this.selected = options.selected - this.#container.classList.toggle('disabled', this.disabled) - this.#container.classList.toggle('selected', this.selected) - this.element.textContent = this.name - this.element.title = this.title + if (this.#container) { + this.#container.classList.toggle('disabled', this.disabled) + this.#container.classList.toggle('selected', this.selected) + this.element.textContent = this.name + this.element.title = this.title + } } #maskOnTap (puzzle, tile) { diff --git a/src/components/puzzle.js b/src/components/puzzle.js index 6e12b4b..0f9a453 100644 --- a/src/components/puzzle.js +++ b/src/components/puzzle.js @@ -156,6 +156,8 @@ export class Puzzle { } mask.onMask(this) + + document.body.classList.add(Puzzle.Events.Mask) } select (id) { @@ -179,6 +181,8 @@ export class Puzzle { this.#mask.onUnmask(this) this.#mask = undefined + document.body.classList.remove(Puzzle.Events.Mask) + const mask = this.#maskQueue.pop() if (mask) { // Evaluate after any current events have processed (e.g. beam updates from last mask) diff --git a/src/components/state.js b/src/components/state.js index 2e52a5d..79cb474 100644 --- a/src/components/state.js +++ b/src/components/state.js @@ -95,6 +95,7 @@ export class State { this.#current = structuredClone(this.#original) this.#deltas = [] this.#index = this.#lastIndex() + this.#selectedTile = undefined State.clearCache(this.getId()) diff --git a/src/puzzles/011.js b/src/puzzles/011.js index f7fdafa..0b6e4fc 100644 --- a/src/puzzles/011.js +++ b/src/puzzles/011.js @@ -5,7 +5,6 @@ export default { { items: [ { - direction: 5, type: 'Portal' } ], @@ -50,12 +49,12 @@ export default { { items: [ { - direction: 2, + direction: 0, type: 'Portal' } ], modifiers: [ - { type: 'Lock' } + { type: 'Rotate' } ], type: 'Tile' }, @@ -88,7 +87,7 @@ export default { } ], modifiers: [ - { type: 'Rotate' } + { type: 'Lock' } ], type: 'Tile' }, @@ -110,5 +109,6 @@ export default { }, solution: [ { amount: 1, type: 'Connections' } - ] + ], + version: 1 } diff --git a/test/fixtures.js b/test/fixtures.js index b00b2f2..e8fab2e 100644 --- a/test/fixtures.js +++ b/test/fixtures.js @@ -1,6 +1,11 @@ require('chromedriver') const chrome = require('selenium-webdriver/chrome') -const { Builder, By, until, WebElementCondition } = require('selenium-webdriver') +const { Builder, By, Condition, logging, until } = require('selenium-webdriver') + +logging.installConsoleHandler() + +const logger = logging.getLogger('') +logger.setLevel(logging.Level.DEBUG) class PuzzleFixture { driver @@ -28,13 +33,17 @@ class PuzzleFixture { '--disable-dev-shm-usage', '--disable-extensions', '--disable-gpu', - '--headless=new', + '--headless', '--ignore-certificate-errors', + '--no-sandbox', '--window-size=768,1024' ) console.log('Building driver...') - this.driver = await new Builder().forBrowser('chrome').setChromeOptions(options).build() + this.driver = await new Builder() + .forBrowser('chrome') + .setChromeOptions(options) + .build() console.log(`Getting URL: ${this.url}`) await this.driver.get(this.url) @@ -60,13 +69,21 @@ class PuzzleFixture { await this.driver.actions({ async: true }).move({ origin: this.elements.canvas }).click().perform() } + async isMasked () { + return this.driver.wait(untilElementHasClass(this.elements.body, 'puzzle-mask')) + } + + async isNotMasked () { + return this.driver.wait(untilElementDoesNotHaveClass(this.elements.body, 'puzzle-mask')) + } + async isSolved () { - return elementHasClass(this.elements.body, 'puzzle-solved') + return this.driver.wait(untilElementHasClass(this.elements.body, 'puzzle-solved')) } async selectModifier (name) { const origin = this.#getModifier(name) - await this.driver.actions({ async: true }).move({ origin }).press().pause(500).release().perform() + await this.driver.actions({ async: true }).move({ origin }).press().pause(501).release().perform() } async #getModifier (name) { @@ -77,12 +94,18 @@ class PuzzleFixture { static baseUrl = 'http://localhost:1234' } -function elementHasClass (element, name) { - return new WebElementCondition('until element has class', function () { +function untilElementHasClass (element, name) { + return new Condition('until element has class', function () { return element.getAttribute('class').then((classes) => classes.split(' ').some((className) => name === className)) }) } +function untilElementDoesNotHaveClass (element, name) { + return new Condition('until element does not have class', function () { + return element.getAttribute('class').then((classes) => classes.split(' ').every((className) => name !== className)) + }) +} + module.exports = { PuzzleFixture } diff --git a/test/puzzles/011.js b/test/puzzles/011.js index cf491df..32a5a31 100644 --- a/test/puzzles/011.js +++ b/test/puzzles/011.js @@ -9,13 +9,15 @@ describe('Puzzle 011', function () { before(puzzle.before) it('should be solved', async function () { - await puzzle.clickTile(2, 0) - await puzzle.clickModifier('rotate', { times: 3 }) - await puzzle.clickTile(0, 1) + await puzzle.clickTile(1, 1) + await puzzle.clickModifier('rotate', { times: 2 }) + await puzzle.isMasked() + await puzzle.clickTile(2, 1) + await puzzle.isNotMasked() - await puzzle.clickTile(2, 0) + await puzzle.clickTile(1, 1) await puzzle.selectModifier('rotate') - await puzzle.clickTile(0, 0) + await puzzle.clickTile(2, 0) await puzzle.clickModifier('rotate', { times: 3 }) assert(await puzzle.isSolved())