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.
Files changed (191) hide show
  1. package/.env +1 -0
  2. package/.next/BUILD_ID +1 -0
  3. package/.next/build-manifest.json +42 -0
  4. package/.next/dynamic-css-manifest.json +8 -0
  5. package/.next/export-marker.json +6 -0
  6. package/.next/images-manifest.json +68 -0
  7. package/.next/package.json +1 -0
  8. package/.next/prerender-manifest.json +28 -0
  9. package/.next/react-loadable-manifest.json +182 -0
  10. package/.next/required-server-files.js +371 -0
  11. package/.next/required-server-files.json +371 -0
  12. package/.next/routes-manifest.json +119 -0
  13. package/.next/server/chunks/30.js +6 -0
  14. package/.next/server/chunks/309.js +1 -0
  15. package/.next/server/chunks/344.js +1 -0
  16. package/.next/server/chunks/371.js +1 -0
  17. package/.next/server/chunks/465.js +1 -0
  18. package/.next/server/chunks/546.js +1 -0
  19. package/.next/server/chunks/612.js +5 -0
  20. package/.next/server/chunks/623.js +1 -0
  21. package/.next/server/chunks/641.js +1 -0
  22. package/.next/server/chunks/67.js +1 -0
  23. package/.next/server/chunks/671.js +1 -0
  24. package/.next/server/chunks/692.js +1 -0
  25. package/.next/server/chunks/705.js +1 -0
  26. package/.next/server/chunks/712.js +1 -0
  27. package/.next/server/chunks/715.js +1 -0
  28. package/.next/server/chunks/812.js +1 -0
  29. package/.next/server/chunks/846.js +1 -0
  30. package/.next/server/chunks/853.js +15 -0
  31. package/.next/server/chunks/886.js +1 -0
  32. package/.next/server/chunks/923.js +1 -0
  33. package/.next/server/chunks/942.js +1 -0
  34. package/.next/server/chunks/953.js +1 -0
  35. package/.next/server/chunks/961.js +1 -0
  36. package/.next/server/chunks/968.js +1 -0
  37. package/.next/server/chunks/97.js +1 -0
  38. package/.next/server/dynamic-css-manifest.js +1 -0
  39. package/.next/server/functions-config-manifest.json +4 -0
  40. package/.next/server/interception-route-rewrite-manifest.js +1 -0
  41. package/.next/server/middleware-build-manifest.js +1 -0
  42. package/.next/server/middleware-manifest.json +6 -0
  43. package/.next/server/middleware-react-loadable-manifest.js +1 -0
  44. package/.next/server/next-font-manifest.js +1 -0
  45. package/.next/server/next-font-manifest.json +1 -0
  46. package/.next/server/pages/404.html +29 -0
  47. package/.next/server/pages/404.js +28 -0
  48. package/.next/server/pages/500.html +29 -0
  49. package/.next/server/pages/_app.js +1 -0
  50. package/.next/server/pages/_document.js +28 -0
  51. package/.next/server/pages/_error.js +28 -0
  52. package/.next/server/pages/api/dictionary/[locale]/[word].js +2 -0
  53. package/.next/server/pages/api/dictionary/[locale].js +1 -0
  54. package/.next/server/pages/api/solve.js +1 -0
  55. package/.next/server/pages/api/verify.js +1 -0
  56. package/.next/server/pages/api/visit.js +1 -0
  57. package/.next/server/pages/index.html +29 -0
  58. package/.next/server/pages/index.js +30 -0
  59. package/.next/server/pages/index.json +1 -0
  60. package/.next/server/pages/index.meta +1 -0
  61. package/.next/server/pages-manifest.json +12 -0
  62. package/.next/server/prefetch-hints.json +1 -0
  63. package/.next/server/preview-props.json +5 -0
  64. package/.next/server/route-cache/PAGES/8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1/$/index.html +29 -0
  65. package/.next/server/route-cache/PAGES/8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1/$/index.json +1 -0
  66. package/.next/server/route-cache/PAGES/8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1/$/index.meta +1 -0
  67. package/.next/server/webpack-api-runtime.js +1 -0
  68. package/.next/server/webpack-runtime.js +1 -0
  69. package/.next/static/Ng_NN9gQywZowmg2O0pgm/_buildManifest.js +1 -0
  70. package/.next/static/Ng_NN9gQywZowmg2O0pgm/_ssgManifest.js +1 -0
  71. package/.next/static/chunks/100.52dda92e5e8a952a.js +1 -0
  72. package/.next/static/chunks/277.6785dfaf22fdb0b7.js +1 -0
  73. package/.next/static/chunks/307.8fdeb1732d749981.js +1 -0
  74. package/.next/static/chunks/341.79535ba85bf1986f.js +1 -0
  75. package/.next/static/chunks/363.25b64181ebca205d.js +1 -0
  76. package/.next/static/chunks/422.8327da4bbddd0842.js +1 -0
  77. package/.next/static/chunks/519.9f9c2d29933bf2ef.js +1 -0
  78. package/.next/static/chunks/541.c69629e483176ad7.js +1 -0
  79. package/.next/static/chunks/628.3175e816bc17d5c7.js +1 -0
  80. package/.next/static/chunks/631.3623a2830ab30d1e.js +1 -0
  81. package/.next/static/chunks/663.cfbde775630d10fd.js +1 -0
  82. package/.next/static/chunks/693.55a42abad20cbdaa.js +1 -0
  83. package/.next/static/chunks/73.73bfdde6a7a9b42b.js +1 -0
  84. package/.next/static/chunks/744-5baf05ff91b58ebe.js +3 -0
  85. package/.next/static/chunks/820.e8f3e1c85e1657ec.js +1 -0
  86. package/.next/static/chunks/827.d01b6d69af340ba4.js +1 -0
  87. package/.next/static/chunks/865.5265cef552d53595.js +1 -0
  88. package/.next/static/chunks/912.b802c1066a130cf6.js +1 -0
  89. package/.next/static/chunks/939.63d71e74e359a311.js +1 -0
  90. package/.next/static/chunks/980.dd35e807b18063c1.js +1 -0
  91. package/.next/static/chunks/990.61296e5870b91f11.js +1 -0
  92. package/.next/static/chunks/framework-86d3b250d278cea9.js +1 -0
  93. package/.next/static/chunks/main-738e097cd313908f.js +5 -0
  94. package/.next/static/chunks/pages/404-8013c66eb40999e8.js +1 -0
  95. package/.next/static/chunks/pages/_app-aee589671f9cc846.js +1 -0
  96. package/.next/static/chunks/pages/_error-f0290a98eb3ed0ec.js +1 -0
  97. package/.next/static/chunks/pages/index-a96995844e778716.js +1 -0
  98. package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  99. package/.next/static/chunks/webpack-4bca6879a7d98e24.js +1 -0
  100. package/.next/static/css/2d03f0da2c678dfc.css +1 -0
  101. package/.next/static/css/4dbcacc1122ac6b9.css +2 -0
  102. package/.next/static/css/7b6724d2df70f48f.css +1 -0
  103. package/.next/static/css/90e8d51d917ed7e3.css +1 -0
  104. package/.next/static/css/98051ed25076b896.css +1 -0
  105. package/.next/static/css/b736be3bd02b5d99.css +1 -0
  106. package/.next/static/css/ca311e6c030663d4.css +1 -0
  107. package/.next/static/css/cd4d7f2458487399.css +1 -0
  108. package/.next/static/css/dee878eb64099d92.css +1 -0
  109. package/README.md +5 -217
  110. package/bin/scrabble-solver.js +35 -4
  111. package/next.config.js +60 -0
  112. package/package.json +92 -52
  113. package/public/favicon.ico +0 -0
  114. package/public/icons/apple-touch-icon-114x114.png +0 -0
  115. package/public/icons/apple-touch-icon-120x120.png +0 -0
  116. package/public/icons/apple-touch-icon-144x144.png +0 -0
  117. package/public/icons/apple-touch-icon-152x152.png +0 -0
  118. package/public/icons/apple-touch-icon-57x57.png +0 -0
  119. package/public/icons/apple-touch-icon-60x60.png +0 -0
  120. package/public/icons/apple-touch-icon-72x72.png +0 -0
  121. package/public/icons/apple-touch-icon-76x76.png +0 -0
  122. package/public/icons/favicon-128.png +0 -0
  123. package/public/icons/favicon-16x16.png +0 -0
  124. package/public/icons/favicon-196x196.png +0 -0
  125. package/public/icons/favicon-32x32.png +0 -0
  126. package/public/icons/favicon-96x96.png +0 -0
  127. package/public/icons/favicon.svg +6 -0
  128. package/public/icons/mstile-144x144.png +0 -0
  129. package/public/icons/mstile-150x150.png +0 -0
  130. package/public/icons/mstile-310x150.png +0 -0
  131. package/public/icons/mstile-310x310.png +0 -0
  132. package/public/icons/mstile-70x70.png +0 -0
  133. package/public/llms.txt +11 -0
  134. package/public/logo.svg +62 -0
  135. package/public/og.png +0 -0
  136. package/public/robots.txt +5 -0
  137. package/public/service-worker.js +2 -0
  138. package/public/site.webmanifest +14 -0
  139. package/public/sitemap.xml +1 -0
  140. package/.gitattributes +0 -2
  141. package/.next/trace +0 -1
  142. package/.next/trace-build +0 -1
  143. package/.oxfmtrc.json +0 -17
  144. package/.oxlintrc.json +0 -314
  145. package/CLAUDE.md +0 -134
  146. package/HISTORY.md +0 -13
  147. package/bun-test-globals.d.ts +0 -1
  148. package/bun.lock +0 -2150
  149. package/bun.test.preload.ts +0 -21
  150. package/bunfig.toml +0 -9
  151. package/e2e/app.spec.ts +0 -124
  152. package/e2e/bugs/112-character-bonus.spec.ts +0 -25
  153. package/e2e/bugs/129-esc-sidebar.spec.ts +0 -17
  154. package/e2e/bugs/2-q-tile-in-english.spec.ts +0 -19
  155. package/e2e/bugs/3-x-tile-in-polish.spec.ts +0 -19
  156. package/e2e/bugs/30-click-inside-radio-icon.spec.ts +0 -25
  157. package/e2e/bugs/333-disabled-radio-cursor.spec.ts +0 -18
  158. package/e2e/bugs/342-results-sorting.spec.ts +0 -30
  159. package/e2e/bugs/352-dictionary-race-condition.spec.ts +0 -27
  160. package/e2e/bugs/363-dictionary-input-unique-words.spec.ts +0 -27
  161. package/e2e/bugs/380-transliteration-when-pasting.spec.ts +0 -55
  162. package/e2e/bugs/398-digraphs-in-created-words.spec.ts +0 -23
  163. package/e2e/bugs/401-board-state-on-game-change.spec.ts +0 -24
  164. package/e2e/bugs/445-tile-text-selection.spec.ts +0 -38
  165. package/e2e/bugs/451-results-list-gap-hover.spec.ts +0 -35
  166. package/e2e/bugs/452-stale-translations-cache.spec.ts +0 -32
  167. package/e2e/bugs/461-regexp-highlight.spec.ts +0 -33
  168. package/e2e/features/198-highlight-unreachable-cells.spec.ts +0 -84
  169. package/e2e/features/427-highlight-created-words.spec.ts +0 -86
  170. package/e2e/features/430-highlight-unreachable-cells-results.spec.ts +0 -74
  171. package/e2e/features/91-hover-remaining-tile-highlights.spec.ts +0 -97
  172. package/e2e/features/hidden-coordinates-migration.spec.ts +0 -27
  173. package/e2e/features/result-candidate-picker.spec.ts +0 -21
  174. package/e2e/features/results-interactions.spec.ts +0 -170
  175. package/e2e/features/solver-paths.spec.ts +0 -45
  176. package/e2e/features/words-dictionary-search.spec.ts +0 -36
  177. package/e2e/features/words-empty-state.spec.ts +0 -42
  178. package/e2e/features/words-interactions.spec.ts +0 -179
  179. package/e2e/features/words-preview.spec.ts +0 -140
  180. package/e2e/features/words-table-filtering.spec.ts +0 -22
  181. package/e2e/features/words-table-sorting.spec.ts +0 -33
  182. package/e2e/lib/actions.ts +0 -202
  183. package/e2e/lib/index.ts +0 -2
  184. package/e2e/lib/selectors.ts +0 -108
  185. package/e2e/tsconfig.json +0 -10
  186. package/nx.json +0 -39
  187. package/playwright.config.ts +0 -30
  188. package/project.json +0 -3
  189. package/screencast.gif +0 -0
  190. package/scripts/record-screencast.ts +0 -347
  191. package/tsconfig.json +0 -19
@@ -1,84 +0,0 @@
1
- import { type Page, expect, test } from '@playwright/test';
2
-
3
- import * as Lib from '../lib';
4
-
5
- /*
6
- * @see https://github.com/kamilmielnik/scrabble-solver/issues/198
7
- */
8
- test.describe('#198 - Highlight unreachable cells', () => {
9
- test('has no effect when the rack is empty', async ({ page }) => {
10
- await Lib.visitIndex(page);
11
- await Lib.typeBoard(page, 'cat', 'horizontal', { x: 7, y: 7 });
12
- await setHighlightUnreachableCells(page, 'On');
13
-
14
- await expectReachable(page, 0, 0);
15
- await expectReachable(page, 14, 14);
16
- await expectReachable(page, 7, 7);
17
- await expectReachable(page, 7, 8);
18
- });
19
-
20
- test('has no effect when the setting is off', async ({ page }) => {
21
- await Lib.visitIndex(page);
22
- await Lib.typeRack(page, 'abcdefg');
23
-
24
- await expectReachable(page, 0, 0);
25
- await expectReachable(page, 14, 14);
26
- await expectReachable(page, 7, 7);
27
- });
28
-
29
- test('dims unreachable cells on an empty board when rack has tiles', async ({ page }) => {
30
- await Lib.visitIndex(page);
31
- await Lib.typeRack(page, 'abc');
32
- await setHighlightUnreachableCells(page, 'On');
33
-
34
- await expectReachable(page, 7, 7);
35
- await expectReachable(page, 5, 7);
36
- await expectReachable(page, 7, 5);
37
-
38
- await expectUnreachable(page, 0, 0);
39
- await expectUnreachable(page, 14, 14);
40
- await expectUnreachable(page, 4, 7);
41
- await expectUnreachable(page, 6, 6);
42
- });
43
-
44
- test('dims cells far from any placed tile or rack reach', async ({ page }) => {
45
- await Lib.visitIndex(page);
46
- await Lib.typeBoard(page, 'cat', 'horizontal', { x: 7, y: 7 });
47
- await Lib.typeRack(page, 'ab');
48
- await setHighlightUnreachableCells(page, 'On');
49
-
50
- await expectReachable(page, 7, 7);
51
- await expectReachable(page, 6, 7);
52
- await expectReachable(page, 10, 7);
53
- await expectReachable(page, 8, 6);
54
-
55
- await expectUnreachable(page, 0, 0);
56
- await expectUnreachable(page, 14, 14);
57
- await expectUnreachable(page, 0, 14);
58
- });
59
-
60
- test('updates dimming when toggled off after being on', async ({ page }) => {
61
- await Lib.visitIndex(page);
62
- await Lib.typeRack(page, 'abc');
63
- await setHighlightUnreachableCells(page, 'On');
64
- await expectUnreachable(page, 0, 0);
65
-
66
- await setHighlightUnreachableCells(page, 'Off');
67
- await expectReachable(page, 0, 0);
68
- await expectReachable(page, 14, 14);
69
- });
70
- });
71
-
72
- async function setHighlightUnreachableCells(page: Page, value: 'Off' | 'On') {
73
- await Lib.openSettingsModal(page);
74
- await Lib.getSettingOption(page, 'Highlight unreachable cells', value).check();
75
- await Lib.closeModal(page);
76
- }
77
-
78
- async function expectUnreachable(page: Page, x: number, y: number) {
79
- await expect(Lib.getBoardTile(page, x, y).locator('..')).toHaveClass(/unreachable/);
80
- }
81
-
82
- async function expectReachable(page: Page, x: number, y: number) {
83
- await expect(Lib.getBoardTile(page, x, y).locator('..')).not.toHaveClass(/unreachable/);
84
- }
@@ -1,86 +0,0 @@
1
- import { expect, test } from '@playwright/test';
2
-
3
- import * as Lib from '../lib';
4
-
5
- /*
6
- * @see https://github.com/kamilmielnik/scrabble-solver/issues/427
7
- */
8
- test.describe('#427 - Highlight created words on board on hover', () => {
9
- test('highlights the hovered word only at its own position', async ({ page }) => {
10
- await Lib.visitIndex(page);
11
- await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
12
- await Lib.typeBoard(page, 'cat', 'vertical', { x: 8, y: 8 });
13
- await Lib.openWordsModal(page);
14
-
15
- await Lib.hoverWord(page, 3, 3, 'horizontal');
16
- await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 3));
17
- await Lib.expectTileHighlighted(Lib.getBoardTile(page, 4, 3));
18
- await Lib.expectTileHighlighted(Lib.getBoardTile(page, 5, 3));
19
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 8, 8));
20
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 8, 9));
21
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 8, 10));
22
-
23
- await Lib.hoverWord(page, 8, 8, 'vertical');
24
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 3));
25
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 4, 3));
26
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 5, 3));
27
- await Lib.expectTileHighlighted(Lib.getBoardTile(page, 8, 8));
28
- await Lib.expectTileHighlighted(Lib.getBoardTile(page, 8, 9));
29
- await Lib.expectTileHighlighted(Lib.getBoardTile(page, 8, 10));
30
-
31
- await Lib.moveMouseAway(page);
32
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 3));
33
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 4, 3));
34
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 5, 3));
35
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 8, 8));
36
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 8, 9));
37
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 8, 10));
38
- });
39
-
40
- test('highlights a hovered invalid word', async ({ page }) => {
41
- await Lib.visitIndex(page);
42
- await Lib.typeBoard(page, 'zvq', 'horizontal', { x: 5, y: 5 });
43
- await Lib.openWordsModal(page);
44
-
45
- await expect(page.getByTestId('word-5-5-horizontal').getByLabel('Invalid', { exact: true })).toBeVisible();
46
- await Lib.hoverWord(page, 5, 5, 'horizontal');
47
- await Lib.expectTileHighlighted(Lib.getBoardTile(page, 5, 5));
48
- await Lib.expectTileHighlighted(Lib.getBoardTile(page, 6, 5));
49
- await Lib.expectTileHighlighted(Lib.getBoardTile(page, 7, 5));
50
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 8, 5));
51
- });
52
-
53
- test('clears the highlight when the modal closes', async ({ page }) => {
54
- await Lib.visitIndex(page);
55
- await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
56
- await Lib.openWordsModal(page);
57
-
58
- await Lib.hoverWord(page, 3, 3, 'horizontal');
59
- await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 3));
60
-
61
- await page.keyboard.press('Escape');
62
- await expect(Lib.getOpenModal(page)).toHaveCount(0);
63
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 3));
64
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 4, 3));
65
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 5, 3));
66
- });
67
-
68
- test.describe('narrow enough to select the first word', () => {
69
- test.use({ viewport: { width: 800, height: 900 } });
70
-
71
- test('highlights the clicked word and keeps it highlighted when the pointer leaves', async ({ page }) => {
72
- await Lib.visitIndex(page);
73
- await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
74
- await Lib.typeBoard(page, 'dog', 'horizontal', { x: 3, y: 5 });
75
- await Lib.openWordsModal(page);
76
-
77
- await page.getByTestId('word-3-5-horizontal').click();
78
- await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 5));
79
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 3));
80
-
81
- await Lib.moveMouseAway(page);
82
- await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 5));
83
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 3));
84
- });
85
- });
86
- });
@@ -1,74 +0,0 @@
1
- import { type Page, expect, test } from '@playwright/test';
2
-
3
- import * as Lib from '../lib';
4
-
5
- /*
6
- * @see https://github.com/kamilmielnik/scrabble-solver/issues/430
7
- */
8
- test.describe('#430 - Highlight unreachable cells includes current results', () => {
9
- test('narrows the highlight to cells covered by results after solving', async ({ page }) => {
10
- await Lib.visitIndex(page);
11
- await Lib.typeBoard(page, 'i', 'horizontal', { x: 7, y: 7 });
12
- await Lib.typeRack(page, 'q');
13
- await setHighlightUnreachableCells(page, 'On');
14
-
15
- await expectReachable(page, 6, 7);
16
- await expectReachable(page, 8, 7);
17
- await expectReachable(page, 7, 6);
18
- await expectReachable(page, 7, 8);
19
-
20
- await Lib.solve(page);
21
- await expect(Lib.getResults(page)).toHaveCount(2);
22
-
23
- await expectReachable(page, 7, 7);
24
- await expectReachable(page, 6, 7);
25
- await expectReachable(page, 7, 6);
26
-
27
- await expectUnreachable(page, 8, 7);
28
- await expectUnreachable(page, 7, 8);
29
- await expectUnreachable(page, 0, 0);
30
- });
31
-
32
- test('falls back to the heuristic when results become outdated', async ({ page }) => {
33
- await Lib.visitIndex(page);
34
- await Lib.typeBoard(page, 'i', 'horizontal', { x: 7, y: 7 });
35
- await Lib.typeRack(page, 'q');
36
- await setHighlightUnreachableCells(page, 'On');
37
- await Lib.solve(page);
38
- await expectUnreachable(page, 8, 7);
39
-
40
- await Lib.typeRack(page, 'a', 1);
41
-
42
- await expectReachable(page, 8, 7);
43
- await expectReachable(page, 7, 8);
44
- await expectUnreachable(page, 0, 0);
45
- });
46
-
47
- test('dims all cells when fresh results are empty', async ({ page }) => {
48
- await Lib.visitIndex(page);
49
- await Lib.typeRack(page, 'q');
50
- await setHighlightUnreachableCells(page, 'On');
51
- await expectReachable(page, 7, 7);
52
-
53
- await Lib.solve(page);
54
- await expect(Lib.getResults(page)).toHaveCount(0);
55
-
56
- await expectUnreachable(page, 7, 7);
57
- await expectUnreachable(page, 6, 7);
58
- await expectUnreachable(page, 0, 0);
59
- });
60
- });
61
-
62
- async function setHighlightUnreachableCells(page: Page, value: 'Off' | 'On') {
63
- await Lib.openSettingsModal(page);
64
- await Lib.getSettingOption(page, 'Highlight unreachable cells', value).check();
65
- await Lib.closeModal(page);
66
- }
67
-
68
- async function expectUnreachable(page: Page, x: number, y: number) {
69
- await expect(Lib.getBoardTile(page, x, y).locator('..')).toHaveClass(/unreachable/);
70
- }
71
-
72
- async function expectReachable(page: Page, x: number, y: number) {
73
- await expect(Lib.getBoardTile(page, x, y).locator('..')).not.toHaveClass(/unreachable/);
74
- }
@@ -1,97 +0,0 @@
1
- import { type Page, expect, test } from '@playwright/test';
2
-
3
- import * as Lib from '../lib';
4
-
5
- /*
6
- * @see https://github.com/kamilmielnik/scrabble-solver/issues/91
7
- */
8
- test.describe('#91 - Hovering remaining tile highlights all same tiles on the board and rack', () => {
9
- test('highlights matching letter tiles on the board and rack', async ({ page }) => {
10
- await Lib.visitIndex(page);
11
- await Lib.typeBoard(page, 'cat', 'horizontal', { x: 7, y: 7 });
12
- await Lib.typeRack(page, 'aab ');
13
- await openRemainingTilesModal(page);
14
-
15
- await hoverRemainingTile(page, 'a');
16
- await Lib.expectTileHighlighted(Lib.getRackTile(page, 0));
17
- await Lib.expectTileHighlighted(Lib.getRackTile(page, 1));
18
- await Lib.expectTileNotHighlighted(Lib.getRackTile(page, 2));
19
- await Lib.expectTileNotHighlighted(Lib.getRackTile(page, 3));
20
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 7, 7));
21
- await Lib.expectTileHighlighted(Lib.getBoardTile(page, 8, 7));
22
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 9, 7));
23
-
24
- await hoverRemainingTile(page, 'b');
25
- await Lib.expectTileNotHighlighted(Lib.getRackTile(page, 0));
26
- await Lib.expectTileNotHighlighted(Lib.getRackTile(page, 1));
27
- await Lib.expectTileHighlighted(Lib.getRackTile(page, 2));
28
- await Lib.expectTileNotHighlighted(Lib.getRackTile(page, 3));
29
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 7, 7));
30
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 8, 7));
31
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 9, 7));
32
- });
33
-
34
- test('highlights matching blank tiles on the board and rack', async ({ page }) => {
35
- await Lib.visitIndex(page);
36
- await Lib.typeBoard(page, 'a', 'horizontal', { x: 7, y: 7 });
37
- await Lib.getBoardTile(page, 7, 7).focus();
38
- await Lib.typeBoard(page, ' ', 'horizontal', { x: 7, y: 7 });
39
- await Lib.typeRack(page, ' a');
40
- await openRemainingTilesModal(page);
41
-
42
- await hoverRemainingTile(page, ' ');
43
- await Lib.expectTileHighlighted(Lib.getRackTile(page, 0));
44
- await Lib.expectTileNotHighlighted(Lib.getRackTile(page, 1));
45
- await Lib.expectTileHighlighted(Lib.getBoardTile(page, 7, 7));
46
-
47
- await hoverRemainingTile(page, 'a');
48
- await Lib.expectTileNotHighlighted(Lib.getRackTile(page, 0));
49
- await Lib.expectTileHighlighted(Lib.getRackTile(page, 1));
50
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 7, 7));
51
- });
52
-
53
- test('does not highlight a remaining tile in the sidebar if it has not been used', async ({ page }) => {
54
- await Lib.visitIndex(page);
55
- await Lib.typeRack(page, 'a');
56
- await openRemainingTilesModal(page);
57
-
58
- await hoverRemainingTile(page, 'z');
59
- await expect(page.getByTestId('remaining-tile-z').locator('[role="mark"]')).toHaveCount(0);
60
- await Lib.expectTileNotHighlighted(Lib.getRackTile(page, 0));
61
-
62
- await hoverRemainingTile(page, 'a');
63
- await expect(page.getByTestId('remaining-tile-a').locator('[role="mark"]')).not.toHaveCount(0);
64
- await Lib.expectTileHighlighted(Lib.getRackTile(page, 0));
65
- });
66
-
67
- test.describe('full-viewport modal', () => {
68
- test.use({ viewport: { width: 420, height: 900 } });
69
-
70
- test('does not highlight anything when the modal covers the viewport', async ({ page }) => {
71
- await Lib.visitIndex(page);
72
- await Lib.typeBoard(page, 'cat', 'horizontal', { x: 7, y: 7 });
73
- await Lib.typeRack(page, 'aab ');
74
- await openRemainingTilesModalFromMenu(page);
75
-
76
- await hoverRemainingTile(page, 'a');
77
- await expect(page.getByTestId('remaining-tile-a').locator('[role="mark"]')).toHaveCount(0);
78
- await Lib.expectTileNotHighlighted(Lib.getRackTile(page, 0));
79
- await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 8, 7));
80
- });
81
- });
82
- });
83
-
84
- async function openRemainingTilesModal(page: Page) {
85
- await Lib.openModal(page, page.getByLabel('Remaining tiles', { exact: true }));
86
- }
87
-
88
- async function openRemainingTilesModalFromMenu(page: Page) {
89
- await Lib.openMenuModal(page);
90
- await Lib.openModal(page, Lib.getOpenModal(page).getByLabel('Remaining tiles', { exact: true }));
91
- await expect(page.getByTestId('remaining-tile-a')).toBeVisible();
92
- }
93
-
94
- async function hoverRemainingTile(page: Page, character: string) {
95
- const testId = character === ' ' ? 'remaining-tile-blank' : `remaining-tile-${character}`;
96
- await page.getByTestId(testId).hover();
97
- }
@@ -1,27 +0,0 @@
1
- import { expect, test } from '@playwright/test';
2
-
3
- import * as Lib from '../lib';
4
-
5
- /*
6
- * The 'hidden' showCoordinates option was removed on 2026/08/13.
7
- * Delete this spec together with migrateHiddenShowCoordinates.
8
- */
9
- test.describe('"hidden" coordinates migration', () => {
10
- test('falls back to original coordinates when storage holds the removed "hidden" option', async ({ page }) => {
11
- await page.addInitScript(() => {
12
- localStorage.setItem('scrabble-solver.settings', JSON.stringify({ showCoordinates: 'hidden' }));
13
- });
14
-
15
- await Lib.visitIndex(page);
16
-
17
- await expect(Lib.getBoardContainer(page).getByText('A', { exact: true })).toBeVisible();
18
-
19
- await Lib.openSettingsModal(page);
20
- await expect(Lib.getSettingOption(page, 'Coordinates', 'Original')).toBeChecked();
21
- await Lib.closeModal(page);
22
-
23
- await expect
24
- .poll(() => page.evaluate(() => localStorage.getItem('scrabble-solver.settings')))
25
- .toContain('"showCoordinates":"original"');
26
- });
27
- });
@@ -1,21 +0,0 @@
1
- import { expect, test } from '@playwright/test';
2
-
3
- import * as Lib from '../lib';
4
-
5
- test.describe('Result candidate picker', () => {
6
- test.use({ viewport: { width: 800, height: 900 } });
7
-
8
- test('shows a placeholder until a result candidate is picked', async ({ page }) => {
9
- await Lib.visitIndex(page);
10
-
11
- const picker = Lib.getResultCandidatePicker(page);
12
- await expect(picker).toHaveText('Select...');
13
-
14
- await Lib.typeBoard(page, 'i', 'horizontal', { x: 7, y: 7 });
15
- await Lib.typeRack(page, 'q');
16
- await Lib.solve(page);
17
-
18
- await expect(picker).not.toContainText('Select...');
19
- await expect(picker).toContainText('qi');
20
- });
21
- });
@@ -1,170 +0,0 @@
1
- import { type Page, expect, test } from '@playwright/test';
2
-
3
- import * as Lib from '../lib';
4
-
5
- test.describe('Results interactions', () => {
6
- test.describe('compact, non-touch', () => {
7
- test.use({ hasTouch: false, viewport: { width: 800, height: 900 } });
8
-
9
- test('marks rows as clickable', expectModalRowsToBeClickable);
10
-
11
- test('leaves the candidate untouched while the pointer travels over the list', async ({ page }) => {
12
- await solveInSidebar(page);
13
- await Lib.openResultsModal(page);
14
- await expect(Lib.getModalResult(page, 0)).toHaveAttribute('aria-current', 'true');
15
-
16
- await Lib.hoverModalResult(page, 1);
17
- await expect(Lib.getModalResult(page, 0)).toHaveAttribute('aria-current', 'true');
18
- await expect(Lib.getModalResult(page, 1)).not.toHaveAttribute('aria-current');
19
-
20
- await Lib.moveMouseAway(page);
21
- await expect(Lib.getModalResult(page, 0)).toHaveAttribute('aria-current', 'true');
22
- });
23
-
24
- test('picks the clicked result and keeps the modal open when it is clicked again', async ({ page }) => {
25
- await solveInSidebar(page);
26
- await Lib.openResultsModal(page);
27
-
28
- await Lib.getModalResult(page, 1).click();
29
- await expect(Lib.getModalResult(page, 1)).toHaveAttribute('aria-current', 'true');
30
- await expect(Lib.getModalResult(page, 0)).not.toHaveAttribute('aria-current');
31
-
32
- await Lib.getModalResult(page, 1).click();
33
- await Lib.expectModalToStayOpen(page);
34
- await expect(Lib.getModalResult(page, 1)).toHaveAttribute('aria-current', 'true');
35
- });
36
-
37
- test('previews the candidate', expectPreviewToKeepTheCandidate);
38
-
39
- test('inserts the candidate', expectInsertToPlaceTheCandidate);
40
- });
41
-
42
- test.describe('compact, touch', () => {
43
- test.use({ hasTouch: true, viewport: { width: 800, height: 900 } });
44
-
45
- test('marks rows as clickable', expectModalRowsToBeClickable);
46
-
47
- test('picks the tapped result and previews it when it is tapped again', async ({ page }) => {
48
- await solveInSidebar(page);
49
- await Lib.openResultsModal(page);
50
-
51
- await Lib.getModalResult(page, 1).click();
52
- await expect(Lib.getModalResult(page, 1)).toHaveAttribute('aria-current', 'true');
53
- await Lib.expectModalToStayOpen(page);
54
-
55
- await Lib.getModalResult(page, 1).click();
56
- await expect(Lib.getModal(page)).toHaveCount(0);
57
- await expect(Lib.getResultCandidatePicker(page)).toContainText('qi');
58
- await expect(Lib.getRackTile(page)).toHaveValue('q');
59
- });
60
-
61
- test('previews the candidate', expectPreviewToKeepTheCandidate);
62
-
63
- test('inserts the candidate', expectInsertToPlaceTheCandidate);
64
- });
65
-
66
- test.describe('desktop, non-touch', () => {
67
- test.use({ hasTouch: false, viewport: { width: 1440, height: 900 } });
68
-
69
- test('offers no preview or insert button', expectNoResultButtons);
70
-
71
- test('marks rows as clickable', expectSidebarRowsToBeClickable);
72
-
73
- test('previews the hovered result and drops it when the pointer leaves the list', async ({ page }) => {
74
- await solveInSidebar(page);
75
-
76
- await Lib.hoverResult(page, 0);
77
- await expect(Lib.getResult(page, 0)).toHaveAttribute('aria-current', 'true');
78
- await expect(page.locator('[role="mark"]').first()).toBeVisible();
79
-
80
- await Lib.hoverResult(page, 1);
81
- await expect(Lib.getResult(page, 1)).toHaveAttribute('aria-current', 'true');
82
- await expect(Lib.getResult(page, 0)).not.toHaveAttribute('aria-current');
83
-
84
- await Lib.moveMouseAway(page);
85
- await expect(Lib.getResult(page, 1)).not.toHaveAttribute('aria-current');
86
- await expect(page.locator('[role="mark"]')).toHaveCount(0);
87
- });
88
-
89
- test('places the clicked result on the board', async ({ page }) => {
90
- await solveInSidebar(page);
91
-
92
- await Lib.getResult(page, 0).click();
93
- await Lib.expectBoardCharacters(page, ['q', 'i']);
94
- await expect(Lib.getRackTile(page)).toHaveValue('');
95
- });
96
- });
97
-
98
- test.describe('desktop, touch', () => {
99
- test.use({ hasTouch: true, viewport: { width: 1440, height: 900 } });
100
-
101
- test('offers no preview or insert button', expectNoResultButtons);
102
-
103
- test('marks rows as clickable', expectSidebarRowsToBeClickable);
104
-
105
- test('picks the tapped result and places it on the board when it is tapped again', async ({ page }) => {
106
- await solveInSidebar(page);
107
-
108
- await Lib.getResult(page, 0).click();
109
- await expect(Lib.getResult(page, 0)).toHaveAttribute('aria-current', 'true');
110
- await expect(Lib.getRackTile(page)).toHaveValue('q');
111
-
112
- await Lib.getResult(page, 0).click();
113
- await Lib.expectBoardCharacters(page, ['q', 'i']);
114
- await expect(Lib.getRackTile(page)).toHaveValue('');
115
- });
116
- });
117
- });
118
-
119
- async function expectModalRowsToBeClickable({ page }: { page: Page }): Promise<void> {
120
- await solveInSidebar(page);
121
- await Lib.openResultsModal(page);
122
-
123
- await Lib.expectRowClickable(Lib.getModalResult(page), true);
124
- }
125
-
126
- async function expectSidebarRowsToBeClickable({ page }: { page: Page }): Promise<void> {
127
- await solveInSidebar(page);
128
-
129
- await Lib.expectRowClickable(Lib.getResult(page), true);
130
- }
131
-
132
- async function expectPreviewToKeepTheCandidate({ page }: { page: Page }): Promise<void> {
133
- await solveInSidebar(page);
134
- await Lib.openResultsModal(page);
135
- await Lib.getModalResult(page, 1).click();
136
-
137
- await Lib.getOpenModal(page).getByRole('button', { name: 'Preview', exact: true }).click();
138
- await expect(Lib.getModal(page)).toHaveCount(0);
139
- await expect(Lib.getResultCandidatePicker(page)).toContainText('qi');
140
- await expect(Lib.getRackTile(page)).toHaveValue('q');
141
- await expect(page.locator('[role="mark"]').first()).toBeVisible();
142
- }
143
-
144
- async function expectNoResultButtons({ page }: { page: Page }): Promise<void> {
145
- await solveInSidebar(page);
146
-
147
- await expect(Lib.getResultsContainer(page)).toBeVisible();
148
- await expect(page.getByRole('button', { name: 'Preview', exact: true })).toHaveCount(0);
149
- await expect(page.getByRole('button', { name: 'Insert', exact: true })).toHaveCount(0);
150
- }
151
-
152
- async function expectInsertToPlaceTheCandidate({ page }: { page: Page }): Promise<void> {
153
- await solveInSidebar(page);
154
- await Lib.openResultsModal(page);
155
- await expect(Lib.getModalResult(page, 0)).toHaveAttribute('aria-current', 'true');
156
-
157
- await Lib.getOpenModal(page).getByRole('button', { name: 'Insert', exact: true }).click();
158
- await expect(Lib.getModal(page)).toHaveCount(0);
159
- await Lib.expectBoardCharacters(page, ['q', 'i']);
160
- await expect(Lib.getRackTile(page)).toHaveValue('');
161
- await expect(Lib.getResultCandidatePicker(page)).toHaveText('Select...');
162
- }
163
-
164
- async function solveInSidebar(page: Page): Promise<void> {
165
- await Lib.preferKeyboardInput(page);
166
- await Lib.visitIndex(page);
167
- await Lib.typeBoard(page, 'i', 'horizontal', { x: 7, y: 7 });
168
- await Lib.typeRack(page, 'q');
169
- await Lib.solve(page);
170
- }
@@ -1,45 +0,0 @@
1
- import { type Page, expect, test } from '@playwright/test';
2
-
3
- import * as Lib from '../lib';
4
-
5
- test.describe('Solver paths', () => {
6
- test('solves through the server when no dictionary is available locally', async ({ page }) => {
7
- await page.route('**/api/dictionary/*', (route) => route.fulfill({ status: 503 }));
8
- const solveResponse = page.waitForResponse('**/api/solve');
9
- await Lib.visitIndex(page);
10
- await Lib.typeBoard(page, 'i', 'horizontal', { x: 7, y: 7 });
11
- await Lib.typeRack(page, 'q');
12
- await Lib.solve(page);
13
- await solveResponse;
14
-
15
- await expect(Lib.getResults(page)).toHaveCount(2);
16
- await Lib.assertResult(page, 0, 'qi', 11);
17
- });
18
-
19
- test('solves locally once the dictionary is cached', async ({ page }) => {
20
- await Lib.visitIndex(page);
21
- await Lib.typeBoard(page, 'i', 'horizontal', { x: 7, y: 7 });
22
- await Lib.typeRack(page, 'q');
23
- await waitForCachedDictionary(page);
24
-
25
- let solveRequestCount = 0;
26
- await page.route('**/api/solve', (route) => {
27
- solveRequestCount += 1;
28
- return route.continue();
29
- });
30
- await Lib.solve(page);
31
-
32
- await expect(Lib.getResults(page)).toHaveCount(2);
33
- await Lib.assertResult(page, 0, 'qi', 11);
34
- expect(solveRequestCount).toBe(0);
35
- });
36
- });
37
-
38
- function waitForCachedDictionary(page: Page): Promise<void> {
39
- return expect.poll(() => page.evaluate(isDictionaryCached), { timeout: 30000 }).toBe(true);
40
- }
41
-
42
- async function isDictionaryCached(): Promise<boolean> {
43
- const cache = await caches.open('dictionary-api-cache');
44
- return typeof (await cache.match('/api/dictionary/en-US')) !== 'undefined';
45
- }
@@ -1,36 +0,0 @@
1
- import { expect, test } from '@playwright/test';
2
-
3
- import * as Lib from '../lib';
4
-
5
- test.describe('Words dictionary search', () => {
6
- test('searches the hovered word and its collisions in the dictionary', async ({ page }) => {
7
- await Lib.visitIndex(page);
8
- await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
9
- await Lib.typeBoard(page, 'od', 'vertical', { x: 3, y: 4 });
10
- await Lib.typeBoard(page, 'dog', 'horizontal', { x: 8, y: 8 });
11
- await Lib.openWordsModal(page);
12
-
13
- await Lib.hoverWord(page, 3, 3, 'horizontal');
14
- await expect(Lib.getDictionaryInput(page)).toHaveValue('cat, cod');
15
-
16
- await Lib.hoverWord(page, 3, 3, 'vertical');
17
- await expect(Lib.getDictionaryInput(page)).toHaveValue('cod, cat');
18
-
19
- await Lib.hoverWord(page, 8, 8, 'horizontal');
20
- await expect(Lib.getDictionaryInput(page)).toHaveValue('dog');
21
-
22
- await expect(Lib.getModalDictionary(page)).toBeHidden();
23
- });
24
-
25
- test.describe('mobile', () => {
26
- test.use({ viewport: { width: 800, height: 900 } });
27
-
28
- test('shows the dictionary below the words table', async ({ page }) => {
29
- await Lib.visitIndex(page);
30
- await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
31
- await Lib.openWordsModal(page);
32
-
33
- await expect(Lib.getModalDictionary(page)).toBeVisible();
34
- });
35
- });
36
- });