scrabble-solver 2.18.10-alpha.1 → 2.19.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +1 -0
- package/.next/BUILD_ID +1 -0
- package/.next/build-manifest.json +42 -0
- package/.next/dynamic-css-manifest.json +8 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +68 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +28 -0
- package/.next/react-loadable-manifest.json +182 -0
- package/.next/required-server-files.js +371 -0
- package/.next/required-server-files.json +371 -0
- package/.next/routes-manifest.json +119 -0
- package/.next/server/chunks/30.js +6 -0
- package/.next/server/chunks/309.js +1 -0
- package/.next/server/chunks/344.js +1 -0
- package/.next/server/chunks/371.js +1 -0
- package/.next/server/chunks/465.js +1 -0
- package/.next/server/chunks/546.js +1 -0
- package/.next/server/chunks/612.js +5 -0
- package/.next/server/chunks/623.js +1 -0
- package/.next/server/chunks/641.js +1 -0
- package/.next/server/chunks/67.js +1 -0
- package/.next/server/chunks/671.js +1 -0
- package/.next/server/chunks/692.js +1 -0
- package/.next/server/chunks/705.js +1 -0
- package/.next/server/chunks/712.js +1 -0
- package/.next/server/chunks/715.js +1 -0
- package/.next/server/chunks/812.js +1 -0
- package/.next/server/chunks/846.js +1 -0
- package/.next/server/chunks/853.js +15 -0
- package/.next/server/chunks/886.js +1 -0
- package/.next/server/chunks/923.js +1 -0
- package/.next/server/chunks/942.js +1 -0
- package/.next/server/chunks/953.js +1 -0
- package/.next/server/chunks/961.js +1 -0
- package/.next/server/chunks/968.js +1 -0
- package/.next/server/chunks/97.js +1 -0
- package/.next/server/dynamic-css-manifest.js +1 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +29 -0
- package/.next/server/pages/404.js +28 -0
- package/.next/server/pages/500.html +29 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_document.js +28 -0
- package/.next/server/pages/_error.js +28 -0
- package/.next/server/pages/api/dictionary/[locale]/[word].js +2 -0
- package/.next/server/pages/api/dictionary/[locale].js +1 -0
- package/.next/server/pages/api/solve.js +1 -0
- package/.next/server/pages/api/verify.js +1 -0
- package/.next/server/pages/api/visit.js +1 -0
- package/.next/server/pages/index.html +29 -0
- package/.next/server/pages/index.js +30 -0
- package/.next/server/pages/index.json +1 -0
- package/.next/server/pages/index.meta +1 -0
- package/.next/server/pages-manifest.json +12 -0
- package/.next/server/prefetch-hints.json +1 -0
- package/.next/server/preview-props.json +5 -0
- package/.next/server/route-cache/PAGES/8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1/$/index.html +29 -0
- package/.next/server/route-cache/PAGES/8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1/$/index.json +1 -0
- package/.next/server/route-cache/PAGES/8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1/$/index.meta +1 -0
- package/.next/server/webpack-api-runtime.js +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/Ng_NN9gQywZowmg2O0pgm/_buildManifest.js +1 -0
- package/.next/static/Ng_NN9gQywZowmg2O0pgm/_ssgManifest.js +1 -0
- package/.next/static/chunks/100.52dda92e5e8a952a.js +1 -0
- package/.next/static/chunks/277.6785dfaf22fdb0b7.js +1 -0
- package/.next/static/chunks/307.8fdeb1732d749981.js +1 -0
- package/.next/static/chunks/341.79535ba85bf1986f.js +1 -0
- package/.next/static/chunks/363.25b64181ebca205d.js +1 -0
- package/.next/static/chunks/422.8327da4bbddd0842.js +1 -0
- package/.next/static/chunks/519.9f9c2d29933bf2ef.js +1 -0
- package/.next/static/chunks/541.c69629e483176ad7.js +1 -0
- package/.next/static/chunks/628.3175e816bc17d5c7.js +1 -0
- package/.next/static/chunks/631.3623a2830ab30d1e.js +1 -0
- package/.next/static/chunks/663.cfbde775630d10fd.js +1 -0
- package/.next/static/chunks/693.55a42abad20cbdaa.js +1 -0
- package/.next/static/chunks/73.73bfdde6a7a9b42b.js +1 -0
- package/.next/static/chunks/744-5baf05ff91b58ebe.js +3 -0
- package/.next/static/chunks/820.e8f3e1c85e1657ec.js +1 -0
- package/.next/static/chunks/827.d01b6d69af340ba4.js +1 -0
- package/.next/static/chunks/865.5265cef552d53595.js +1 -0
- package/.next/static/chunks/912.b802c1066a130cf6.js +1 -0
- package/.next/static/chunks/939.63d71e74e359a311.js +1 -0
- package/.next/static/chunks/980.dd35e807b18063c1.js +1 -0
- package/.next/static/chunks/990.61296e5870b91f11.js +1 -0
- package/.next/static/chunks/framework-86d3b250d278cea9.js +1 -0
- package/.next/static/chunks/main-738e097cd313908f.js +5 -0
- package/.next/static/chunks/pages/404-8013c66eb40999e8.js +1 -0
- package/.next/static/chunks/pages/_app-aee589671f9cc846.js +1 -0
- package/.next/static/chunks/pages/_error-f0290a98eb3ed0ec.js +1 -0
- package/.next/static/chunks/pages/index-a96995844e778716.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-4bca6879a7d98e24.js +1 -0
- package/.next/static/css/2d03f0da2c678dfc.css +1 -0
- package/.next/static/css/4dbcacc1122ac6b9.css +2 -0
- package/.next/static/css/7b6724d2df70f48f.css +1 -0
- package/.next/static/css/90e8d51d917ed7e3.css +1 -0
- package/.next/static/css/98051ed25076b896.css +1 -0
- package/.next/static/css/b736be3bd02b5d99.css +1 -0
- package/.next/static/css/ca311e6c030663d4.css +1 -0
- package/.next/static/css/cd4d7f2458487399.css +1 -0
- package/.next/static/css/dee878eb64099d92.css +1 -0
- package/README.md +5 -217
- package/bin/scrabble-solver.js +35 -4
- package/next.config.js +60 -0
- package/package.json +92 -52
- package/public/favicon.ico +0 -0
- package/public/icons/apple-touch-icon-114x114.png +0 -0
- package/public/icons/apple-touch-icon-120x120.png +0 -0
- package/public/icons/apple-touch-icon-144x144.png +0 -0
- package/public/icons/apple-touch-icon-152x152.png +0 -0
- package/public/icons/apple-touch-icon-57x57.png +0 -0
- package/public/icons/apple-touch-icon-60x60.png +0 -0
- package/public/icons/apple-touch-icon-72x72.png +0 -0
- package/public/icons/apple-touch-icon-76x76.png +0 -0
- package/public/icons/favicon-128.png +0 -0
- package/public/icons/favicon-16x16.png +0 -0
- package/public/icons/favicon-196x196.png +0 -0
- package/public/icons/favicon-32x32.png +0 -0
- package/public/icons/favicon-96x96.png +0 -0
- package/public/icons/favicon.svg +6 -0
- package/public/icons/mstile-144x144.png +0 -0
- package/public/icons/mstile-150x150.png +0 -0
- package/public/icons/mstile-310x150.png +0 -0
- package/public/icons/mstile-310x310.png +0 -0
- package/public/icons/mstile-70x70.png +0 -0
- package/public/llms.txt +11 -0
- package/public/logo.svg +62 -0
- package/public/og.png +0 -0
- package/public/robots.txt +5 -0
- package/public/service-worker.js +2 -0
- package/public/site.webmanifest +14 -0
- package/public/sitemap.xml +1 -0
- package/.gitattributes +0 -2
- package/.next/trace +0 -1
- package/.next/trace-build +0 -1
- package/.oxfmtrc.json +0 -17
- package/.oxlintrc.json +0 -314
- package/CLAUDE.md +0 -134
- package/HISTORY.md +0 -13
- package/bun-test-globals.d.ts +0 -1
- package/bun.lock +0 -2150
- package/bun.test.preload.ts +0 -21
- package/bunfig.toml +0 -9
- package/e2e/app.spec.ts +0 -124
- package/e2e/bugs/112-character-bonus.spec.ts +0 -25
- package/e2e/bugs/129-esc-sidebar.spec.ts +0 -17
- package/e2e/bugs/2-q-tile-in-english.spec.ts +0 -19
- package/e2e/bugs/3-x-tile-in-polish.spec.ts +0 -19
- package/e2e/bugs/30-click-inside-radio-icon.spec.ts +0 -25
- package/e2e/bugs/333-disabled-radio-cursor.spec.ts +0 -18
- package/e2e/bugs/342-results-sorting.spec.ts +0 -30
- package/e2e/bugs/352-dictionary-race-condition.spec.ts +0 -27
- package/e2e/bugs/363-dictionary-input-unique-words.spec.ts +0 -27
- package/e2e/bugs/380-transliteration-when-pasting.spec.ts +0 -55
- package/e2e/bugs/398-digraphs-in-created-words.spec.ts +0 -23
- package/e2e/bugs/401-board-state-on-game-change.spec.ts +0 -24
- package/e2e/bugs/445-tile-text-selection.spec.ts +0 -38
- package/e2e/bugs/451-results-list-gap-hover.spec.ts +0 -35
- package/e2e/bugs/452-stale-translations-cache.spec.ts +0 -32
- package/e2e/bugs/461-regexp-highlight.spec.ts +0 -33
- package/e2e/features/198-highlight-unreachable-cells.spec.ts +0 -84
- package/e2e/features/427-highlight-created-words.spec.ts +0 -86
- package/e2e/features/430-highlight-unreachable-cells-results.spec.ts +0 -74
- package/e2e/features/91-hover-remaining-tile-highlights.spec.ts +0 -97
- package/e2e/features/hidden-coordinates-migration.spec.ts +0 -27
- package/e2e/features/result-candidate-picker.spec.ts +0 -21
- package/e2e/features/results-interactions.spec.ts +0 -170
- package/e2e/features/solver-paths.spec.ts +0 -45
- package/e2e/features/words-dictionary-search.spec.ts +0 -36
- package/e2e/features/words-empty-state.spec.ts +0 -42
- package/e2e/features/words-interactions.spec.ts +0 -179
- package/e2e/features/words-preview.spec.ts +0 -140
- package/e2e/features/words-table-filtering.spec.ts +0 -22
- package/e2e/features/words-table-sorting.spec.ts +0 -33
- package/e2e/lib/actions.ts +0 -202
- package/e2e/lib/index.ts +0 -2
- package/e2e/lib/selectors.ts +0 -108
- package/e2e/tsconfig.json +0 -10
- package/nx.json +0 -39
- package/playwright.config.ts +0 -30
- package/project.json +0 -3
- package/screencast.gif +0 -0
- package/scripts/record-screencast.ts +0 -347
- package/tsconfig.json +0 -19
package/e2e/lib/selectors.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { type Locator, type Page } from '@playwright/test';
|
|
2
|
-
|
|
3
|
-
export function getModal(page: Page): Locator {
|
|
4
|
-
return page.getByRole('dialog');
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function getOpenModal(page: Page): Locator {
|
|
8
|
-
return page.locator('.ReactModal__Overlay--after-open:not(.ReactModal__Overlay--before-close)').getByRole('dialog');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function getBoardContainer(page: Page): Locator {
|
|
12
|
-
return page.getByTestId('board');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function getBoardTile(page: Page, x = 0, y = 0, boardWidth = 15): Locator {
|
|
16
|
-
const index = y * boardWidth + x;
|
|
17
|
-
return getBoardContainer(page).getByRole('textbox', { includeHidden: true }).nth(index);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function getDictionary(page: Page): Locator {
|
|
21
|
-
return page.getByTestId('dictionary');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function getDictionaryInput(page: Page): Locator {
|
|
25
|
-
return getDictionary(page).getByRole('textbox', {
|
|
26
|
-
includeHidden: true, // so that the dictionary can be asserted on while modal is opened
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function getDictionaryTitles(page: Page): Locator {
|
|
31
|
-
return getDictionary(page).locator('h2');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function getModalDictionary(page: Page): Locator {
|
|
35
|
-
return getOpenModal(page).locator('[class*="dictionary"]');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function getLoading(page: Page): Locator {
|
|
39
|
-
return page.getByTestId('loading');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function getRackContainer(page: Page): Locator {
|
|
43
|
-
return page.getByTestId('rack');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function getRackTile(page: Page, index = 0): Locator {
|
|
47
|
-
return getRackContainer(page)
|
|
48
|
-
.getByRole('textbox', {
|
|
49
|
-
includeHidden: true, // so that rack can be interacted with while modal is opened
|
|
50
|
-
})
|
|
51
|
-
.nth(index);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function getResultsContainer(page: Page): Locator {
|
|
55
|
-
return page.getByTestId('results');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function getResults(page: Page): Locator {
|
|
59
|
-
return getResultsContainer(page).getByTestId('result');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function getResultsFilterInput(page: Page): Locator {
|
|
63
|
-
return getResultsContainer(page).getByRole('textbox');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function getResult(page: Page, index = 0): Locator {
|
|
67
|
-
return getResults(page).nth(index);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function getResultCandidatePicker(page: Page): Locator {
|
|
71
|
-
return page.getByTestId('result-candidate-picker');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function getModalResults(page: Page): Locator {
|
|
75
|
-
return getOpenModal(page).getByTestId('result');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function getModalResult(page: Page, index = 0): Locator {
|
|
79
|
-
return getModalResults(page).nth(index);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function getWordsContainer(page: Page): Locator {
|
|
83
|
-
return page.getByTestId('words');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function getWords(page: Page): Locator {
|
|
87
|
-
return getWordsContainer(page).locator('[data-testid^="word-"]');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function getWord(page: Page, index = 0): Locator {
|
|
91
|
-
return getWords(page).nth(index);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function getWordsFilterInput(page: Page): Locator {
|
|
95
|
-
return getWordsContainer(page).getByRole('textbox');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function getSettingsButton(page: Page): Locator {
|
|
99
|
-
return page.getByTestId('settings-button');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function getSettingOption(page: Page, section: string, option: string): Locator {
|
|
103
|
-
return getOpenModal(page).getByLabel(section, { exact: true }).getByLabel(option, { exact: true });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function getTooltip(page: Page): Locator {
|
|
107
|
-
return page.getByRole('tooltip');
|
|
108
|
-
}
|
package/e2e/tsconfig.json
DELETED
package/nx.json
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"targetDefaults": {
|
|
3
|
-
"build": {
|
|
4
|
-
"dependsOn": [
|
|
5
|
-
"^build"
|
|
6
|
-
],
|
|
7
|
-
"cache": true
|
|
8
|
-
}
|
|
9
|
-
},
|
|
10
|
-
"release": {
|
|
11
|
-
"projects": [
|
|
12
|
-
"scrabble-solver",
|
|
13
|
-
"@scrabble-solver/*"
|
|
14
|
-
],
|
|
15
|
-
"projectsRelationship": "fixed",
|
|
16
|
-
"version": {
|
|
17
|
-
"currentVersionResolver": "disk"
|
|
18
|
-
},
|
|
19
|
-
"releaseTag": {
|
|
20
|
-
"pattern": "{version}"
|
|
21
|
-
},
|
|
22
|
-
"git": {
|
|
23
|
-
"commit": true,
|
|
24
|
-
"commitMessage": "{version}",
|
|
25
|
-
"tag": true,
|
|
26
|
-
"push": false
|
|
27
|
-
},
|
|
28
|
-
"changelog": {
|
|
29
|
-
"projectChangelogs": false,
|
|
30
|
-
"workspaceChangelog": false
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
"pluginsConfig": {
|
|
34
|
-
"@nx/js": {
|
|
35
|
-
"analyzeSourceFiles": false
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"analytics": false
|
|
39
|
-
}
|
package/playwright.config.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
-
|
|
3
|
-
// eslint-disable-next-line no-restricted-exports
|
|
4
|
-
export default defineConfig({
|
|
5
|
-
expect: {
|
|
6
|
-
timeout: 10_000,
|
|
7
|
-
},
|
|
8
|
-
forbidOnly: Boolean(process.env.CI),
|
|
9
|
-
fullyParallel: true,
|
|
10
|
-
projects: [
|
|
11
|
-
{
|
|
12
|
-
name: 'chromium',
|
|
13
|
-
use: {
|
|
14
|
-
...devices['Desktop Chrome'],
|
|
15
|
-
viewport: { height: 900, width: 1440 },
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
],
|
|
19
|
-
reporter: [['list'], ['html', { open: 'never' }]],
|
|
20
|
-
retries: process.env.CI ? 2 : 0,
|
|
21
|
-
testDir: './e2e',
|
|
22
|
-
timeout: 120_000,
|
|
23
|
-
use: {
|
|
24
|
-
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
|
|
25
|
-
screenshot: 'only-on-failure',
|
|
26
|
-
serviceWorkers: 'block',
|
|
27
|
-
trace: 'retain-on-failure',
|
|
28
|
-
},
|
|
29
|
-
workers: process.env.CI ? 2 : undefined,
|
|
30
|
-
});
|
package/project.json
DELETED
package/screencast.gif
DELETED
|
Binary file
|
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regenerates screencast.gif by replaying the oxyphenbutazone scenario in a real browser.
|
|
3
|
-
* Requires the app running at BASE_URL (default http://localhost:3333) - `bun run screencast` boots it.
|
|
4
|
-
*/
|
|
5
|
-
import { type BrowserContext, chromium, type Locator, type Page } from '@playwright/test';
|
|
6
|
-
import { Board } from '@scrabble-solver/types';
|
|
7
|
-
import { execFileSync } from 'node:child_process';
|
|
8
|
-
import { linkSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
-
import os from 'node:os';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
import { fileURLToPath } from 'node:url';
|
|
12
|
-
|
|
13
|
-
interface Point {
|
|
14
|
-
x: number;
|
|
15
|
-
y: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface CapturedFrame {
|
|
19
|
-
file: string;
|
|
20
|
-
timestamp: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const BASE_URL = process.env.BASE_URL ?? 'http://localhost:3333';
|
|
24
|
-
const LOCALE = 'en-GB';
|
|
25
|
-
const WIDTH = 1680;
|
|
26
|
-
const HEIGHT = 1233;
|
|
27
|
-
const CAPTURE_SCALE = 2;
|
|
28
|
-
const FPS = 15;
|
|
29
|
-
const RACK = 'oypbaze';
|
|
30
|
-
const TARGET_WORD = 'oxyphenbutazone';
|
|
31
|
-
const DEFINITIONS_TO_PLACEMENT_DELAY = 1200;
|
|
32
|
-
const DEFINITIONS_REQUEST_DELAY = 1000;
|
|
33
|
-
const CURSOR_START: Point = { x: 1478, y: 1154 };
|
|
34
|
-
const OUTPUT_FILE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'screencast.gif');
|
|
35
|
-
|
|
36
|
-
await recordScreencast();
|
|
37
|
-
|
|
38
|
-
async function recordScreencast(): Promise<void> {
|
|
39
|
-
await ensureAppIsRunning();
|
|
40
|
-
await warmUpDictionary();
|
|
41
|
-
|
|
42
|
-
const framesDirectory = mkdtempSync(path.join(os.tmpdir(), 'screencast-'));
|
|
43
|
-
const browser = await chromium.launch({ args: ['--force-color-profile=srgb'] });
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const context = await browser.newContext({
|
|
47
|
-
deviceScaleFactor: CAPTURE_SCALE,
|
|
48
|
-
locale: LOCALE,
|
|
49
|
-
serviceWorkers: 'block',
|
|
50
|
-
viewport: { height: HEIGHT, width: WIDTH },
|
|
51
|
-
});
|
|
52
|
-
await seedBoard(context);
|
|
53
|
-
await seedSettings(context);
|
|
54
|
-
await delayDefinitionRequests(context);
|
|
55
|
-
await installCursor(context);
|
|
56
|
-
|
|
57
|
-
const page = await context.newPage();
|
|
58
|
-
await preparePage(page);
|
|
59
|
-
|
|
60
|
-
const capture = await startCapture(page, framesDirectory);
|
|
61
|
-
await playScenario(page);
|
|
62
|
-
const endTimestamp = await capture.stop();
|
|
63
|
-
await browser.close();
|
|
64
|
-
|
|
65
|
-
const gifFramesDirectory = resampleFrames(capture.frames, endTimestamp, framesDirectory);
|
|
66
|
-
encodeGif(gifFramesDirectory, OUTPUT_FILE);
|
|
67
|
-
console.log(`Captured ${capture.frames.length} frames, wrote ${OUTPUT_FILE}`);
|
|
68
|
-
} finally {
|
|
69
|
-
await browser.close();
|
|
70
|
-
rmSync(framesDirectory, { force: true, recursive: true });
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function ensureAppIsRunning(): Promise<void> {
|
|
75
|
-
try {
|
|
76
|
-
await fetch(BASE_URL);
|
|
77
|
-
} catch {
|
|
78
|
-
throw new Error(`App is not running at ${BASE_URL} - run "bun run screencast" or start it with "bun start:app"`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function warmUpDictionary(): Promise<void> {
|
|
83
|
-
await fetch(new URL(`/api/dictionary/${LOCALE}`, BASE_URL));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function seedBoard(context: BrowserContext): Promise<void> {
|
|
87
|
-
await context.addInitScript((serializedBoard: string) => {
|
|
88
|
-
window.localStorage.setItem('scrabble-solver.board', JSON.stringify(serializedBoard));
|
|
89
|
-
}, JSON.stringify(createBoard().toJson()));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function seedSettings(context: BrowserContext): Promise<void> {
|
|
93
|
-
await context.addInitScript((locale: string) => {
|
|
94
|
-
window.localStorage.setItem('scrabble-solver.settings', JSON.stringify({ locale }));
|
|
95
|
-
}, LOCALE);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Definitions on localhost load faster than a viewer can read - a latency floor keeps
|
|
100
|
-
* fly-over rows from rendering their definitions and makes the loading state visible.
|
|
101
|
-
*/
|
|
102
|
-
async function delayDefinitionRequests(context: BrowserContext): Promise<void> {
|
|
103
|
-
await context.route('**/api/dictionary/*/*', async (route) => {
|
|
104
|
-
await new Promise((resolve) => setTimeout(resolve, DEFINITIONS_REQUEST_DELAY));
|
|
105
|
-
await route.continue().catch(() => undefined);
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function createBoard(): Board {
|
|
110
|
-
const board = Board.fromStringArray([
|
|
111
|
-
' x hen ut on ',
|
|
112
|
-
'puer or amas j',
|
|
113
|
-
'a led a er a',
|
|
114
|
-
'c ki i elf c',
|
|
115
|
-
'i snot n is u',
|
|
116
|
-
'f t o w do l',
|
|
117
|
-
'y e moa er a',
|
|
118
|
-
'i r solar t',
|
|
119
|
-
'n v h t i',
|
|
120
|
-
'g i i bitten',
|
|
121
|
-
' e n v g',
|
|
122
|
-
' w g e ',
|
|
123
|
-
' e ',
|
|
124
|
-
' d ',
|
|
125
|
-
' ',
|
|
126
|
-
]);
|
|
127
|
-
|
|
128
|
-
board.rows[4][3].tile.isBlank = true;
|
|
129
|
-
board.rows[9][11].tile.isBlank = true;
|
|
130
|
-
|
|
131
|
-
return board;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function installCursor(context: BrowserContext): Promise<void> {
|
|
135
|
-
await context.addInitScript(`(${showFakeCursor.toString()})(${CURSOR_START.x}, ${CURSOR_START.y})`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function showFakeCursor(startX: number, startY: number): void {
|
|
139
|
-
const cursor = document.createElement('div');
|
|
140
|
-
cursor.style.cssText = 'position:fixed;top:0;left:0;z-index:2147483647;pointer-events:none;will-change:transform;';
|
|
141
|
-
cursor.innerHTML =
|
|
142
|
-
'<svg width="21" height="24" viewBox="0 0 14 16" xmlns="http://www.w3.org/2000/svg">' +
|
|
143
|
-
'<polygon points="0.5,0.5 0.5,12.4 3.4,9.7 5.2,14 7.3,13.1 5.5,8.9 9.4,8.7" ' +
|
|
144
|
-
'fill="#000" stroke="#fff" stroke-width="0.8" stroke-linejoin="round"/></svg>';
|
|
145
|
-
cursor.style.transform = `translate(${startX}px, ${startY}px)`;
|
|
146
|
-
|
|
147
|
-
document.addEventListener(
|
|
148
|
-
'mousemove',
|
|
149
|
-
(event) => {
|
|
150
|
-
cursor.style.transform = `translate(${event.clientX}px, ${event.clientY}px)`;
|
|
151
|
-
},
|
|
152
|
-
true,
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
function mount(): void {
|
|
156
|
-
document.documentElement.appendChild(cursor);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (document.readyState === 'loading') {
|
|
160
|
-
document.addEventListener('DOMContentLoaded', mount);
|
|
161
|
-
} else {
|
|
162
|
-
mount();
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function preparePage(page: Page): Promise<void> {
|
|
167
|
-
await page.goto(BASE_URL);
|
|
168
|
-
await page.getByTestId('board').waitFor();
|
|
169
|
-
await page.evaluate(() => document.fonts.ready);
|
|
170
|
-
await page.getByTestId('rack').getByRole('textbox').first().focus();
|
|
171
|
-
await page.mouse.move(CURSOR_START.x, CURSOR_START.y);
|
|
172
|
-
await page.waitForTimeout(500);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function playScenario(page: Page): Promise<void> {
|
|
176
|
-
await page.waitForTimeout(700);
|
|
177
|
-
await page.keyboard.type(RACK, { delay: 160 });
|
|
178
|
-
await page.waitForTimeout(300);
|
|
179
|
-
await page.keyboard.press('Enter');
|
|
180
|
-
await waitForResults(page);
|
|
181
|
-
await page.waitForTimeout(400);
|
|
182
|
-
|
|
183
|
-
const topRowBox = await boxOf(page.getByTestId('results').getByTestId('result').first());
|
|
184
|
-
const hoverX = topRowBox.x + topRowBox.width * 0.8;
|
|
185
|
-
const topRowY = topRowBox.y + topRowBox.height / 2;
|
|
186
|
-
|
|
187
|
-
const cursor = await glide(page, CURSOR_START, { x: hoverX, y: topRowY }, 1800);
|
|
188
|
-
|
|
189
|
-
await waitForDefinitions(page, TARGET_WORD);
|
|
190
|
-
await page.waitForTimeout(DEFINITIONS_TO_PLACEMENT_DELAY);
|
|
191
|
-
await page.mouse.click(cursor.x, cursor.y, { delay: 60 });
|
|
192
|
-
await waitForPlacement(page);
|
|
193
|
-
await page.waitForTimeout(1600);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function waitForResults(page: Page): Promise<unknown> {
|
|
197
|
-
return page.waitForFunction(
|
|
198
|
-
() => {
|
|
199
|
-
const container = document.querySelector('[data-testid="results"]');
|
|
200
|
-
return (
|
|
201
|
-
container?.getAttribute('data-outdated') === 'false' &&
|
|
202
|
-
container.querySelector('[data-testid="result"]') !== null &&
|
|
203
|
-
container.querySelector('[data-testid="loading"]') === null
|
|
204
|
-
);
|
|
205
|
-
},
|
|
206
|
-
undefined,
|
|
207
|
-
{ timeout: 90_000 },
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function waitForDefinitions(page: Page, word: string): Promise<unknown> {
|
|
212
|
-
return page.waitForFunction(
|
|
213
|
-
(expectedWord: string) => {
|
|
214
|
-
const dictionary = document.querySelector('[data-testid="dictionary"]');
|
|
215
|
-
const title = dictionary?.querySelector('h2');
|
|
216
|
-
return (
|
|
217
|
-
title?.textContent?.toLowerCase() === expectedWord &&
|
|
218
|
-
dictionary?.querySelector('[data-testid="loading"]') === null
|
|
219
|
-
);
|
|
220
|
-
},
|
|
221
|
-
word,
|
|
222
|
-
{ timeout: 90_000 },
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function waitForPlacement(page: Page): Promise<unknown> {
|
|
227
|
-
return page.waitForFunction(() => {
|
|
228
|
-
const inputs = document.querySelectorAll<HTMLInputElement>('[data-testid="rack"] input');
|
|
229
|
-
return inputs.length > 0 && [...inputs].every((input) => input.value === '');
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async function glide(page: Page, from: Point, to: Point, durationMs: number): Promise<Point> {
|
|
234
|
-
const distance = Math.hypot(to.x - from.x, to.y - from.y);
|
|
235
|
-
|
|
236
|
-
if (distance === 0) {
|
|
237
|
-
return to;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const bow = Math.min(40, distance * 0.12);
|
|
241
|
-
const normal = { x: -(to.y - from.y) / distance, y: (to.x - from.x) / distance };
|
|
242
|
-
const control = {
|
|
243
|
-
x: (from.x + to.x) / 2 + normal.x * bow,
|
|
244
|
-
y: (from.y + to.y) / 2 + normal.y * bow,
|
|
245
|
-
};
|
|
246
|
-
const steps = Math.max(2, Math.round(durationMs / (1000 / 60)));
|
|
247
|
-
|
|
248
|
-
for (let step = 1; step <= steps; ++step) {
|
|
249
|
-
const progress = easeInOutCubic(step / steps);
|
|
250
|
-
const inverse = 1 - progress;
|
|
251
|
-
const x = inverse * inverse * from.x + 2 * inverse * progress * control.x + progress * progress * to.x;
|
|
252
|
-
const y = inverse * inverse * from.y + 2 * inverse * progress * control.y + progress * progress * to.y;
|
|
253
|
-
await page.mouse.move(x, y);
|
|
254
|
-
await page.waitForTimeout(durationMs / steps);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return to;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function easeInOutCubic(progress: number): number {
|
|
261
|
-
return progress < 0.5 ? 4 * progress ** 3 : 1 - (-2 * progress + 2) ** 3 / 2;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
async function boxOf(locator: Locator): Promise<{ height: number; width: number; x: number; y: number }> {
|
|
265
|
-
const box = await locator.boundingBox();
|
|
266
|
-
|
|
267
|
-
if (!box) {
|
|
268
|
-
throw new Error(`Cannot measure ${String(locator)}`);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return box;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async function startCapture(
|
|
275
|
-
page: Page,
|
|
276
|
-
directory: string,
|
|
277
|
-
): Promise<{ frames: CapturedFrame[]; stop: () => Promise<number> }> {
|
|
278
|
-
const client = await page.context().newCDPSession(page);
|
|
279
|
-
const frames: CapturedFrame[] = [];
|
|
280
|
-
|
|
281
|
-
client.on('Page.screencastFrame', (frame) => {
|
|
282
|
-
const file = path.join(directory, `capture-${String(frames.length).padStart(5, '0')}.png`);
|
|
283
|
-
writeFileSync(file, Buffer.from(frame.data, 'base64'));
|
|
284
|
-
frames.push({ file, timestamp: frame.metadata.timestamp ?? Date.now() / 1000 });
|
|
285
|
-
client.send('Page.screencastFrameAck', { sessionId: frame.sessionId }).catch(() => undefined);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
await client.send('Page.startScreencast', {
|
|
289
|
-
everyNthFrame: 1,
|
|
290
|
-
format: 'png',
|
|
291
|
-
maxHeight: HEIGHT * CAPTURE_SCALE,
|
|
292
|
-
maxWidth: WIDTH * CAPTURE_SCALE,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
return {
|
|
296
|
-
frames,
|
|
297
|
-
stop: async (): Promise<number> => {
|
|
298
|
-
await client.send('Page.stopScreencast');
|
|
299
|
-
await client.detach();
|
|
300
|
-
return Date.now() / 1000;
|
|
301
|
-
},
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function resampleFrames(frames: CapturedFrame[], endTimestamp: number, framesDirectory: string): string {
|
|
306
|
-
if (frames.length === 0) {
|
|
307
|
-
throw new Error('No frames captured');
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const gifFramesDirectory = path.join(framesDirectory, 'gif');
|
|
311
|
-
mkdirSync(gifFramesDirectory);
|
|
312
|
-
|
|
313
|
-
const startTimestamp = frames[0].timestamp;
|
|
314
|
-
const frameCount = Math.max(1, Math.ceil((endTimestamp - startTimestamp) * FPS));
|
|
315
|
-
let sourceIndex = 0;
|
|
316
|
-
|
|
317
|
-
for (let index = 0; index < frameCount; ++index) {
|
|
318
|
-
const tickTimestamp = startTimestamp + index / FPS;
|
|
319
|
-
|
|
320
|
-
while (sourceIndex + 1 < frames.length && frames[sourceIndex + 1].timestamp <= tickTimestamp) {
|
|
321
|
-
++sourceIndex;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
linkSync(frames[sourceIndex].file, path.join(gifFramesDirectory, `frame-${String(index).padStart(5, '0')}.png`));
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return gifFramesDirectory;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function encodeGif(framesDirectory: string, outputFile: string): void {
|
|
331
|
-
execFileSync(
|
|
332
|
-
'ffmpeg',
|
|
333
|
-
[
|
|
334
|
-
'-y',
|
|
335
|
-
'-framerate',
|
|
336
|
-
String(FPS),
|
|
337
|
-
'-i',
|
|
338
|
-
path.join(framesDirectory, 'frame-%05d.png'),
|
|
339
|
-
'-vf',
|
|
340
|
-
`scale=${WIDTH}:${HEIGHT}:flags=lanczos,unsharp=5:5:0.8,split[a][b];[a]palettegen[palette];[b][palette]paletteuse=dither=sierra2_4a:diff_mode=rectangle`,
|
|
341
|
-
'-loop',
|
|
342
|
-
'0',
|
|
343
|
-
outputFile,
|
|
344
|
-
],
|
|
345
|
-
{ stdio: 'inherit' },
|
|
346
|
-
);
|
|
347
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"declaration": true,
|
|
4
|
-
"esModuleInterop": true,
|
|
5
|
-
"forceConsistentCasingInFileNames": true,
|
|
6
|
-
"ignoreDeprecations": "6.0",
|
|
7
|
-
"lib": ["esnext", "dom"],
|
|
8
|
-
"module": "nodenext",
|
|
9
|
-
"moduleResolution": "nodenext",
|
|
10
|
-
"noImplicitReturns": true,
|
|
11
|
-
"noUnusedLocals": true,
|
|
12
|
-
"noUnusedParameters": true,
|
|
13
|
-
"skipLibCheck": true,
|
|
14
|
-
"strict": true,
|
|
15
|
-
"target": "es2021",
|
|
16
|
-
"types": ["bun"]
|
|
17
|
-
},
|
|
18
|
-
"exclude": ["e2e", "node_modules", "playwright.config.ts"]
|
|
19
|
-
}
|