scrabble-solver 2.18.1 → 2.18.3

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 (354) hide show
  1. package/.nx/workspace-data/d/server-process.json +2 -2
  2. package/.nx/workspace-data/file-map.json +1721 -1473
  3. package/.nx/workspace-data/lockfile-dependencies.hash +1 -1
  4. package/.nx/workspace-data/lockfile-nodes.hash +1 -1
  5. package/.nx/workspace-data/nx_files.nxt +0 -0
  6. package/.nx/workspace-data/parsed-lock-file.dependencies.json +184 -209
  7. package/.nx/workspace-data/parsed-lock-file.nodes.json +374 -419
  8. package/.nx/workspace-data/project-graph.json +610 -653
  9. package/.nx/workspace-data/source-maps.json +68 -0
  10. package/CLAUDE.md +13 -11
  11. package/README.md +3 -3
  12. package/bun.lock +115 -130
  13. package/e2e/bugs/398-digraphs-in-created-words.spec.ts +2 -2
  14. package/e2e/bugs/445-tile-text-selection.spec.ts +38 -0
  15. package/e2e/bugs/451-results-list-gap-hover.spec.ts +35 -0
  16. package/e2e/bugs/452-stale-translations-cache.spec.ts +32 -0
  17. package/e2e/features/427-highlight-created-words.spec.ts +86 -0
  18. package/e2e/features/430-highlight-unreachable-cells-results.spec.ts +75 -0
  19. package/e2e/features/91-hover-remaining-tile-highlights.spec.ts +22 -0
  20. package/e2e/features/result-candidate-picker.spec.ts +21 -0
  21. package/e2e/features/results-interactions.spec.ts +170 -0
  22. package/e2e/features/words-dictionary-search.spec.ts +36 -0
  23. package/e2e/features/words-empty-state.spec.ts +42 -0
  24. package/e2e/features/words-interactions.spec.ts +179 -0
  25. package/e2e/features/words-preview.spec.ts +140 -0
  26. package/e2e/features/words-table-filtering.spec.ts +22 -0
  27. package/e2e/features/words-table-sorting.spec.ts +33 -0
  28. package/e2e/lib/actions.ts +76 -6
  29. package/e2e/lib/selectors.ts +40 -3
  30. package/lerna.json +1 -1
  31. package/package.json +10 -10
  32. package/packages/configs/build/games/index.d.ts +1 -0
  33. package/packages/configs/build/games/index.js +1 -0
  34. package/packages/configs/build/games/wyrazy.d.ts +19 -0
  35. package/packages/configs/build/games/wyrazy.js +73 -0
  36. package/packages/configs/build/languages/polish.d.ts +1 -0
  37. package/packages/configs/build/languages/polish.js +39 -1
  38. package/packages/configs/package.json +5 -4
  39. package/packages/configs/src/games/index.ts +1 -0
  40. package/packages/configs/src/games/wyrazy.ts +71 -0
  41. package/packages/configs/src/languages/polish.ts +40 -1
  42. package/packages/constants/package.json +1 -1
  43. package/packages/dictionaries/package.json +4 -4
  44. package/packages/logger/package.json +1 -1
  45. package/packages/logger/scripts/stats.js +52 -31
  46. package/packages/scrabble-solver/.next/BUILD_ID +1 -1
  47. package/packages/scrabble-solver/.next/build-manifest.json +14 -14
  48. package/packages/scrabble-solver/.next/cache/.previewinfo +1 -1
  49. package/packages/scrabble-solver/.next/cache/.rscinfo +1 -1
  50. package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
  51. package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
  52. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
  53. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
  54. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
  55. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  56. package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
  57. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
  58. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
  59. package/packages/scrabble-solver/.next/diagnostics/framework.json +1 -1
  60. package/packages/scrabble-solver/.next/dynamic-css-manifest.json +2 -2
  61. package/packages/scrabble-solver/.next/prerender-manifest.json +4 -4
  62. package/packages/scrabble-solver/.next/react-loadable-manifest.json +37 -37
  63. package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
  64. package/packages/scrabble-solver/.next/server/chunks/309.js +1 -1
  65. package/packages/scrabble-solver/.next/server/chunks/344.js +1 -1
  66. package/packages/scrabble-solver/.next/server/chunks/371.js +1 -1
  67. package/packages/scrabble-solver/.next/server/chunks/392.js +1 -1
  68. package/packages/scrabble-solver/.next/server/chunks/465.js +1 -1
  69. package/packages/scrabble-solver/.next/server/chunks/546.js +1 -1
  70. package/packages/scrabble-solver/.next/server/chunks/641.js +1 -1
  71. package/packages/scrabble-solver/.next/server/chunks/67.js +1 -0
  72. package/packages/scrabble-solver/.next/server/chunks/712.js +1 -1
  73. package/packages/scrabble-solver/.next/server/chunks/812.js +1 -1
  74. package/packages/scrabble-solver/.next/server/chunks/846.js +1 -1
  75. package/packages/scrabble-solver/.next/server/chunks/886.js +1 -1
  76. package/packages/scrabble-solver/.next/server/chunks/942.js +1 -0
  77. package/packages/scrabble-solver/.next/server/chunks/953.js +1 -1
  78. package/packages/scrabble-solver/.next/server/chunks/961.js +1 -1
  79. package/packages/scrabble-solver/.next/server/dynamic-css-manifest.js +1 -1
  80. package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
  81. package/packages/scrabble-solver/.next/server/middleware-react-loadable-manifest.js +1 -1
  82. package/packages/scrabble-solver/.next/server/pages/404.html +3 -3
  83. package/packages/scrabble-solver/.next/server/pages/500.html +3 -3
  84. package/packages/scrabble-solver/.next/server/pages/_app.js +1 -1
  85. package/packages/scrabble-solver/.next/server/pages/_app.js.nft.json +1 -1
  86. package/packages/scrabble-solver/.next/server/pages/_document.js +2 -2
  87. package/packages/scrabble-solver/.next/server/pages/_document.js.nft.json +1 -1
  88. package/packages/scrabble-solver/.next/server/pages/_error.js +1 -1
  89. package/packages/scrabble-solver/.next/server/pages/_error.js.nft.json +1 -1
  90. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js +1 -1
  91. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  92. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js +1 -1
  93. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  94. package/packages/scrabble-solver/.next/server/pages/api/solve.js +1 -1
  95. package/packages/scrabble-solver/.next/server/pages/api/solve.js.nft.json +1 -1
  96. package/packages/scrabble-solver/.next/server/pages/api/verify.js +1 -1
  97. package/packages/scrabble-solver/.next/server/pages/api/verify.js.nft.json +1 -1
  98. package/packages/scrabble-solver/.next/server/pages/index.html +3 -3
  99. package/packages/scrabble-solver/.next/server/pages/index.js +4 -4
  100. package/packages/scrabble-solver/.next/server/pages/index.js.nft.json +1 -1
  101. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  102. package/packages/scrabble-solver/.next/server/pages/not-found.html +3 -3
  103. package/packages/scrabble-solver/.next/server/pages/not-found.js.nft.json +1 -1
  104. package/packages/scrabble-solver/.next/server/pages-manifest.json +1 -1
  105. package/packages/scrabble-solver/.next/static/{5KOWzYqfY1Z2rjrzptr8D → 8WnIUTe-yJvPD6stltvYI}/_buildManifest.js +1 -1
  106. package/packages/scrabble-solver/.next/static/chunks/100.305bdf9c75b1758f.js +1 -0
  107. package/packages/scrabble-solver/.next/static/chunks/256.6a20b23a766cfebd.js +1 -0
  108. package/packages/scrabble-solver/.next/static/chunks/{307.67f52f13e3d60d4e.js → 307.89e6100f58443679.js} +1 -1
  109. package/packages/scrabble-solver/.next/static/chunks/{363.01eed3233f92b6a4.js → 363.cec65b1f9668a75e.js} +1 -1
  110. package/packages/scrabble-solver/.next/static/chunks/{422.1559ae6756912ca5.js → 422.3e55b4c94e328701.js} +1 -1
  111. package/packages/scrabble-solver/.next/static/chunks/{663.d2c9ca36b26695b8.js → 663.bd2c45413fbca518.js} +1 -1
  112. package/packages/scrabble-solver/.next/static/chunks/693.677c88e0eb1340b5.js +1 -0
  113. package/packages/scrabble-solver/.next/static/chunks/{820.97b82f751ca49579.js → 820.823e9e885ebb66af.js} +1 -1
  114. package/packages/scrabble-solver/.next/static/chunks/827.c81b92b383a748c9.js +1 -0
  115. package/packages/scrabble-solver/.next/static/chunks/{865.a9b563a45848a83c.js → 865.59baba8296c9b116.js} +1 -1
  116. package/packages/scrabble-solver/.next/static/chunks/912.077603e4f0bc6cca.js +1 -0
  117. package/packages/scrabble-solver/.next/static/chunks/{939.19b33a3d3c351d73.js → 939.2ad86b50405c7897.js} +1 -1
  118. package/packages/scrabble-solver/.next/static/chunks/980.7ce17464fc56648a.js +1 -0
  119. package/packages/scrabble-solver/.next/static/chunks/{main-b92e08f3fc96dd8d.js → main-7e52da35a4beea0b.js} +1 -1
  120. package/packages/scrabble-solver/.next/static/chunks/pages/_app-e3fe880e24c024ab.js +1 -0
  121. package/packages/scrabble-solver/.next/static/chunks/pages/index-b073a1686818dd26.js +1 -0
  122. package/packages/scrabble-solver/.next/static/chunks/webpack-d7138f1ef6b8c9d6.js +1 -0
  123. package/packages/scrabble-solver/.next/static/css/65bcc9440e067113.css +1 -0
  124. package/packages/scrabble-solver/.next/static/css/{4dbcacc1122ac6b9.css → 92f85ce358a0a468.css} +1 -1
  125. package/packages/scrabble-solver/.next/static/css/a691348b788f8bc0.css +1 -0
  126. package/packages/scrabble-solver/.next/static/css/cd4d7f2458487399.css +1 -0
  127. package/packages/scrabble-solver/.next/trace +22 -21
  128. package/packages/scrabble-solver/.next/trace-build +1 -1
  129. package/packages/scrabble-solver/AGENTS.md +9 -0
  130. package/packages/scrabble-solver/CLAUDE.md +1 -0
  131. package/packages/scrabble-solver/package.json +13 -11
  132. package/packages/scrabble-solver/public/llms.txt +1 -1
  133. package/packages/scrabble-solver/public/service-worker.js +1 -1
  134. package/packages/scrabble-solver/src/components/Board/Board.tsx +3 -1
  135. package/packages/scrabble-solver/src/components/Board/BoardPure.tsx +26 -20
  136. package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.tsx +4 -4
  137. package/packages/scrabble-solver/src/components/Board/lib.test.ts +106 -0
  138. package/packages/scrabble-solver/src/components/Board/lib.ts +34 -3
  139. package/packages/scrabble-solver/src/components/Board/selectors.ts +17 -5
  140. package/packages/scrabble-solver/src/components/Button/Button.module.scss +1 -1
  141. package/packages/scrabble-solver/src/components/Modal/Modal.module.scss +0 -4
  142. package/packages/scrabble-solver/src/components/Results/Header.tsx +30 -5
  143. package/packages/scrabble-solver/src/components/Results/Result.tsx +45 -55
  144. package/packages/scrabble-solver/src/components/Results/Results.module.scss +0 -126
  145. package/packages/scrabble-solver/src/components/Results/Results.module.scss.d.ts +0 -10
  146. package/packages/scrabble-solver/src/components/Results/Results.tsx +25 -5
  147. package/packages/scrabble-solver/src/components/Solver/Solver.module.scss +4 -4
  148. package/packages/scrabble-solver/src/components/Solver/Solver.tsx +5 -3
  149. package/packages/scrabble-solver/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss +5 -1
  150. package/packages/scrabble-solver/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss.d.ts +1 -0
  151. package/packages/scrabble-solver/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +5 -2
  152. package/packages/scrabble-solver/src/components/Table/Cell.tsx +42 -0
  153. package/packages/scrabble-solver/src/components/Table/Header.tsx +11 -0
  154. package/packages/scrabble-solver/src/components/{Results → Table}/HeaderButton.tsx +22 -14
  155. package/packages/scrabble-solver/src/components/Table/Row.tsx +59 -0
  156. package/packages/scrabble-solver/src/components/{ResultsInput/ResultsInput.tsx → Table/Search.tsx} +9 -11
  157. package/packages/scrabble-solver/src/components/Table/Table.module.scss +147 -0
  158. package/packages/scrabble-solver/src/components/Table/Table.module.scss.d.ts +19 -0
  159. package/packages/scrabble-solver/src/components/Table/index.ts +5 -0
  160. package/packages/scrabble-solver/src/components/Tile/Tile.module.scss +2 -1
  161. package/packages/scrabble-solver/src/hooks/useColumns.ts +7 -5
  162. package/packages/scrabble-solver/src/hooks/useIsCompactLayout.ts +14 -0
  163. package/packages/scrabble-solver/src/i18n/languages/english.json +6 -1
  164. package/packages/scrabble-solver/src/i18n/languages/french.json +6 -1
  165. package/packages/scrabble-solver/src/i18n/languages/german.json +6 -1
  166. package/packages/scrabble-solver/src/i18n/languages/persian.json +6 -1
  167. package/packages/scrabble-solver/src/i18n/languages/polish.json +6 -1
  168. package/packages/scrabble-solver/src/i18n/languages/romanian.json +6 -1
  169. package/packages/scrabble-solver/src/i18n/languages/spanish.json +6 -1
  170. package/packages/scrabble-solver/src/i18n/languages/turkish.json +6 -1
  171. package/packages/scrabble-solver/src/icons/QuestionSquare.svg +5 -0
  172. package/packages/scrabble-solver/src/lib/createCoordinatesComparator.ts +23 -0
  173. package/packages/scrabble-solver/src/lib/createKeyComparator.ts +1 -4
  174. package/packages/scrabble-solver/src/lib/createSortComparator.ts +22 -0
  175. package/packages/scrabble-solver/src/lib/getCoordinates.test.ts +21 -0
  176. package/packages/scrabble-solver/src/lib/getCoordinates.ts +16 -0
  177. package/packages/scrabble-solver/src/lib/getNextSort.ts +12 -0
  178. package/packages/scrabble-solver/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +3 -1
  179. package/packages/scrabble-solver/src/modals/RemainingTilesModal/components/Character/Character.tsx +8 -9
  180. package/packages/scrabble-solver/src/modals/ResultsModal/ResultsModal.tsx +14 -10
  181. package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.module.scss +16 -27
  182. package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.module.scss.d.ts +3 -6
  183. package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.tsx +56 -42
  184. package/packages/scrabble-solver/src/modals/WordsModal/components/WordsTable/WordRow.tsx +92 -0
  185. package/packages/scrabble-solver/src/modals/WordsModal/components/WordsTable/WordsTable.module.scss +69 -0
  186. package/packages/scrabble-solver/src/modals/WordsModal/components/WordsTable/WordsTable.module.scss.d.ts +16 -0
  187. package/packages/scrabble-solver/src/modals/WordsModal/components/WordsTable/WordsTable.tsx +137 -0
  188. package/packages/scrabble-solver/src/modals/WordsModal/components/WordsTable/index.ts +1 -0
  189. package/packages/scrabble-solver/src/modals/WordsModal/components/index.ts +1 -0
  190. package/packages/scrabble-solver/src/pages/api/verify.ts +7 -6
  191. package/packages/scrabble-solver/src/pages/index.tsx +29 -4
  192. package/packages/scrabble-solver/src/parameters/index.ts +2 -0
  193. package/packages/scrabble-solver/src/sdk/verify.ts +3 -2
  194. package/packages/scrabble-solver/src/solver-worker/client.test.ts +10 -3
  195. package/packages/scrabble-solver/src/solver-worker/index.worker.ts +7 -7
  196. package/packages/scrabble-solver/src/solver-worker/messages.ts +3 -3
  197. package/packages/scrabble-solver/src/state/hoveredWord/index.ts +4 -0
  198. package/packages/scrabble-solver/src/state/hoveredWord/initialState.ts +5 -0
  199. package/packages/scrabble-solver/src/state/hoveredWord/selectors.ts +5 -0
  200. package/packages/scrabble-solver/src/state/hoveredWord/slice.ts +18 -0
  201. package/packages/scrabble-solver/src/state/hoveredWord/types.ts +5 -0
  202. package/packages/scrabble-solver/src/state/index.ts +1 -0
  203. package/packages/scrabble-solver/src/state/localStorage.ts +13 -2
  204. package/packages/scrabble-solver/src/state/results/lib.ts +9 -26
  205. package/packages/scrabble-solver/src/state/results/selectors.ts +9 -1
  206. package/packages/scrabble-solver/src/state/results/slice.ts +4 -16
  207. package/packages/scrabble-solver/src/state/sagas.ts +26 -1
  208. package/packages/scrabble-solver/src/state/selectors.ts +18 -1
  209. package/packages/scrabble-solver/src/state/store.ts +2 -0
  210. package/packages/scrabble-solver/src/state/types.ts +2 -0
  211. package/packages/scrabble-solver/src/state/verify/initialState.ts +7 -0
  212. package/packages/scrabble-solver/src/state/verify/lib.test.ts +55 -0
  213. package/packages/scrabble-solver/src/state/verify/lib.ts +49 -0
  214. package/packages/scrabble-solver/src/state/verify/selectors.ts +37 -0
  215. package/packages/scrabble-solver/src/state/verify/slice.ts +13 -2
  216. package/packages/scrabble-solver/src/state/verify/types.ts +6 -2
  217. package/packages/scrabble-solver/src/styles/_tokens.scss +2 -0
  218. package/packages/scrabble-solver/src/styles/mixins.scss +5 -0
  219. package/packages/scrabble-solver/src/styles/variables.module.scss +1 -0
  220. package/packages/scrabble-solver/src/styles/variables.module.scss.d.ts +1 -0
  221. package/packages/scrabble-solver/src/styles/variables.scss +1 -1
  222. package/packages/scrabble-solver/src/types/index.ts +24 -11
  223. package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
  224. package/packages/solver/package.json +5 -5
  225. package/packages/types/build/Bingo.d.ts +0 -3
  226. package/packages/types/build/Bingo.js +0 -14
  227. package/packages/types/build/Board.d.ts +3 -1
  228. package/packages/types/build/Board.js +5 -40
  229. package/packages/types/build/BoardJson.d.ts +0 -1
  230. package/packages/types/build/BoardJson.js +0 -19
  231. package/packages/types/build/BoardWord.d.ts +7 -0
  232. package/packages/types/build/BoardWord.js +2 -0
  233. package/packages/types/build/CellJson.d.ts +0 -1
  234. package/packages/types/build/CellJson.js +0 -11
  235. package/packages/types/build/Game.d.ts +2 -2
  236. package/packages/types/build/Game.js +2 -4
  237. package/packages/types/build/Locale.d.ts +0 -1
  238. package/packages/types/build/Locale.js +1 -4
  239. package/packages/types/build/Result.js +3 -3
  240. package/packages/types/build/TileJson.d.ts +0 -1
  241. package/packages/types/build/TileJson.js +0 -6
  242. package/packages/types/build/index.d.ts +9 -8
  243. package/packages/types/build/index.js +14 -17
  244. package/packages/types/build/lib/getBoardWords.d.ts +3 -0
  245. package/packages/types/build/lib/getBoardWords.js +10 -0
  246. package/packages/types/build/lib/getCells.d.ts +4 -0
  247. package/packages/types/build/lib/getCells.js +27 -0
  248. package/packages/types/build/lib/getCollidingWords.d.ts +3 -0
  249. package/packages/types/build/lib/getCollidingWords.js +19 -0
  250. package/packages/types/build/lib/getCollision.d.ts +3 -0
  251. package/packages/types/build/lib/getCollision.js +31 -0
  252. package/packages/types/build/lib/getCollisions.d.ts +3 -0
  253. package/packages/types/build/lib/getCollisions.js +17 -0
  254. package/packages/types/build/lib/getPlacedCell.d.ts +3 -0
  255. package/packages/types/build/lib/getPlacedCell.js +10 -0
  256. package/packages/types/build/lib/getWordThrough.d.ts +4 -0
  257. package/packages/types/build/lib/getWordThrough.js +23 -0
  258. package/packages/types/build/lib/getWordsInRows.d.ts +8 -0
  259. package/packages/types/build/lib/getWordsInRows.js +29 -0
  260. package/packages/types/build/lib/index.d.ts +5 -0
  261. package/packages/types/build/lib/index.js +21 -0
  262. package/packages/types/build/lib/isSameBoardWord.d.ts +2 -0
  263. package/packages/types/build/lib/isSameBoardWord.js +6 -0
  264. package/packages/types/build/lib/toBoardWord.d.ts +4 -0
  265. package/packages/types/build/lib/toBoardWord.js +11 -0
  266. package/packages/types/build/lib/transpose.d.ts +1 -0
  267. package/packages/types/build/lib/transpose.js +16 -0
  268. package/packages/types/build/type-guards/index.d.ts +10 -0
  269. package/packages/types/build/type-guards/index.js +23 -0
  270. package/packages/types/build/type-guards/isBingo.d.ts +2 -0
  271. package/packages/types/build/type-guards/isBingo.js +9 -0
  272. package/packages/types/build/type-guards/isBoardJson.d.ts +2 -0
  273. package/packages/types/build/type-guards/isBoardJson.js +21 -0
  274. package/packages/types/build/type-guards/isCellJson.d.ts +2 -0
  275. package/packages/types/build/type-guards/isCellJson.js +13 -0
  276. package/packages/types/build/type-guards/isGame.d.ts +2 -0
  277. package/packages/types/build/type-guards/isGame.js +7 -0
  278. package/packages/types/build/type-guards/isLocale.d.ts +2 -0
  279. package/packages/types/build/type-guards/isLocale.js +7 -0
  280. package/packages/types/build/type-guards/isMultiplierBingo.d.ts +2 -0
  281. package/packages/types/build/type-guards/isMultiplierBingo.js +8 -0
  282. package/packages/types/build/type-guards/isScoreBingo.d.ts +2 -0
  283. package/packages/types/build/type-guards/isScoreBingo.js +8 -0
  284. package/packages/types/build/type-guards/isTileJson.d.ts +2 -0
  285. package/packages/types/build/type-guards/isTileJson.js +8 -0
  286. package/packages/types/package.json +2 -2
  287. package/packages/types/src/Bingo.ts +0 -14
  288. package/packages/types/src/Board.test.ts +85 -0
  289. package/packages/types/src/Board.ts +8 -49
  290. package/packages/types/src/BoardJson.ts +1 -21
  291. package/packages/types/src/BoardWord.ts +8 -0
  292. package/packages/types/src/CellJson.ts +1 -12
  293. package/packages/types/src/Game.ts +1 -4
  294. package/packages/types/src/Locale.ts +0 -4
  295. package/packages/types/src/Result.ts +3 -3
  296. package/packages/types/src/TileJson.ts +0 -6
  297. package/packages/types/src/index.ts +20 -8
  298. package/packages/types/src/lib/getBoardWords.ts +11 -0
  299. package/packages/types/src/lib/getCells.test.ts +127 -0
  300. package/packages/types/src/lib/getCells.ts +31 -0
  301. package/packages/types/src/lib/getCollidingWords.ts +25 -0
  302. package/packages/types/src/lib/getCollision.ts +38 -0
  303. package/packages/types/src/lib/getCollisions.test.ts +82 -0
  304. package/packages/types/src/lib/getCollisions.ts +22 -0
  305. package/packages/types/src/lib/getPlacedCell.ts +9 -0
  306. package/packages/types/src/lib/getWordThrough.ts +30 -0
  307. package/packages/types/src/lib/getWordsInRows.ts +34 -0
  308. package/packages/types/src/lib/index.ts +5 -0
  309. package/packages/types/src/lib/isSameBoardWord.ts +5 -0
  310. package/packages/types/src/lib/toBoardWord.ts +12 -0
  311. package/packages/types/src/lib/transpose.ts +15 -0
  312. package/packages/types/src/type-guards/index.ts +10 -0
  313. package/packages/types/src/type-guards/isBingo.ts +8 -0
  314. package/packages/types/src/type-guards/isBoardJson.test.ts +33 -0
  315. package/packages/types/src/type-guards/isBoardJson.ts +23 -0
  316. package/packages/types/src/type-guards/isCellJson.ts +14 -0
  317. package/packages/types/src/type-guards/isGame.ts +5 -0
  318. package/packages/types/src/type-guards/isLocale.ts +5 -0
  319. package/packages/types/src/type-guards/isMultiplierBingo.ts +7 -0
  320. package/packages/types/src/type-guards/isScoreBingo.ts +7 -0
  321. package/packages/types/src/type-guards/isTileJson.ts +7 -0
  322. package/packages/word-definitions/package.json +2 -2
  323. package/packages/word-lists/package.json +2 -2
  324. package/playwright-report/index.html +1 -1
  325. package/screencast.gif +0 -0
  326. package/packages/scrabble-solver/.next/server/chunks/316.js +0 -1
  327. package/packages/scrabble-solver/.next/server/chunks/916.js +0 -1
  328. package/packages/scrabble-solver/.next/static/chunks/106.e414e88694add27f.js +0 -1
  329. package/packages/scrabble-solver/.next/static/chunks/127.125d896a19061d71.js +0 -1
  330. package/packages/scrabble-solver/.next/static/chunks/18.8b91e137d239e15c.js +0 -1
  331. package/packages/scrabble-solver/.next/static/chunks/583.7a46807be14c377e.js +0 -1
  332. package/packages/scrabble-solver/.next/static/chunks/711.ba7b8f900dfd40d0.js +0 -1
  333. package/packages/scrabble-solver/.next/static/chunks/912.ff8037ba9d4db5c8.js +0 -1
  334. package/packages/scrabble-solver/.next/static/chunks/pages/_app-2e607dcd4135f340.js +0 -1
  335. package/packages/scrabble-solver/.next/static/chunks/pages/index-e0087247c995328d.js +0 -1
  336. package/packages/scrabble-solver/.next/static/chunks/webpack-cfb34d175e081981.js +0 -1
  337. package/packages/scrabble-solver/.next/static/css/b74cd7de82dabcdd.css +0 -1
  338. package/packages/scrabble-solver/.next/static/css/c403ce739a9c0d83.css +0 -1
  339. package/packages/scrabble-solver/.next/static/css/fbfdce70731d0d2b.css +0 -1
  340. package/packages/scrabble-solver/src/components/Results/Cell.tsx +0 -32
  341. package/packages/scrabble-solver/src/components/ResultsInput/ResultsInput.module.scss +0 -12
  342. package/packages/scrabble-solver/src/components/ResultsInput/ResultsInput.module.scss.d.ts +0 -7
  343. package/packages/scrabble-solver/src/components/ResultsInput/index.ts +0 -1
  344. package/packages/types/build/readResultCells.d.ts +0 -5
  345. package/packages/types/build/readResultCells.js +0 -75
  346. package/packages/types/src/readResultCells.test.ts +0 -140
  347. package/packages/types/src/readResultCells.ts +0 -90
  348. /package/packages/scrabble-solver/.next/static/{5KOWzYqfY1Z2rjrzptr8D → 8WnIUTe-yJvPD6stltvYI}/_ssgManifest.js +0 -0
  349. /package/packages/types/build/{isError.d.ts → type-guards/isError.d.ts} +0 -0
  350. /package/packages/types/build/{isError.js → type-guards/isError.js} +0 -0
  351. /package/packages/types/build/{isObject.d.ts → type-guards/isObject.d.ts} +0 -0
  352. /package/packages/types/build/{isObject.js → type-guards/isObject.js} +0 -0
  353. /package/packages/types/src/{isError.ts → type-guards/isError.ts} +0 -0
  354. /package/packages/types/src/{isObject.ts → type-guards/isObject.ts} +0 -0
@@ -17,7 +17,7 @@ test.describe('#398 - Solitary digraph tiles are recognised as created words', (
17
17
  const modal = Lib.getOpenModal(page);
18
18
  await expect(modal.getByText('ch', { exact: true })).toHaveCount(0);
19
19
  await expect(modal.getByText('chiclean', { exact: true })).toBeVisible();
20
- await expect(modal.getByLabel('Incorrecto', { exact: true }).getByText('0', { exact: true })).toBeVisible();
21
- await expect(modal.getByLabel('Correcto', { exact: true }).getByText('1', { exact: true })).toBeVisible();
20
+ await expect(modal.getByLabel('Incorrecto', { exact: true })).toHaveCount(0);
21
+ await expect(modal.getByLabel('Correcto', { exact: true })).toHaveCount(1);
22
22
  });
23
23
  });
@@ -0,0 +1,38 @@
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/445
7
+ */
8
+ test.describe('#445 - Text selection not disabled in tiles', () => {
9
+ test('Tile input declares user-select: none and a transparent selection highlight', async ({ page }) => {
10
+ await Lib.visitIndex(page);
11
+ const inputClass = await Lib.getBoardTile(page, 0, 0).getAttribute('class');
12
+
13
+ await expect(Lib.getRackTile(page, 0)).toHaveClass(inputClass ?? '');
14
+
15
+ const declarations = await page.evaluate((selector) => {
16
+ const styleBySelector = new Map<string, CSSStyleDeclaration>();
17
+
18
+ for (const sheet of Array.from(document.styleSheets)) {
19
+ for (const rule of Array.from(sheet.cssRules)) {
20
+ if (rule instanceof CSSStyleRule) {
21
+ for (const selectorPart of rule.selectorText.split(',')) {
22
+ styleBySelector.set(selectorPart.trim(), rule.style);
23
+ }
24
+ }
25
+ }
26
+ }
27
+
28
+ return {
29
+ userSelect: styleBySelector.get(selector)?.userSelect,
30
+ selectionBackground: styleBySelector.get(`${selector}::selection`)?.backgroundColor,
31
+ };
32
+ }, `.${inputClass}`);
33
+
34
+ expect(declarations.userSelect).toBe('none');
35
+ // The production build minifies "transparent" to "rgba(0, 0, 0, 0)".
36
+ expect(['transparent', 'rgba(0, 0, 0, 0)']).toContain(declarations.selectionBackground);
37
+ });
38
+ });
@@ -0,0 +1,35 @@
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/451
7
+ */
8
+ test.describe('#451 - Stale highlight below a short results list', () => {
9
+ test('clears the highlight when the pointer moves into the gap below the last result', async ({ page }) => {
10
+ await Lib.visitIndex(page);
11
+ await Lib.typeRack(page, 'qi');
12
+ await Lib.solve(page);
13
+
14
+ const results = Lib.getResults(page);
15
+ await expect(results.first()).toBeVisible();
16
+ await Lib.hoverResult(page, (await results.count()) - 1);
17
+
18
+ const highlightedTiles = page.locator('[role="mark"]');
19
+ await expect(highlightedTiles.first()).toBeVisible();
20
+
21
+ const list = Lib.getResultsContainer(page).locator('[role="list"]');
22
+ const listBox = await list.boundingBox();
23
+ const listContainerBox = await list.locator('..').boundingBox();
24
+
25
+ if (!listBox || !listContainerBox) {
26
+ throw new Error('Expected the results list and its container to be visible');
27
+ }
28
+
29
+ const gapY = listBox.y + listBox.height + 20;
30
+ expect(gapY).toBeLessThan(listContainerBox.y + listContainerBox.height);
31
+ await page.mouse.move(listBox.x + listBox.width / 2, gapY);
32
+
33
+ await expect(highlightedTiles).toHaveCount(0);
34
+ });
35
+ });
@@ -0,0 +1,32 @@
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/452
7
+ */
8
+ test.describe('#452 - Stale translations cache', () => {
9
+ test('recovers when the cached translations are missing a newly added key', async ({ page }) => {
10
+ await Lib.visitIndex(page);
11
+ await page.keyboard.press('Shift');
12
+ await page.waitForFunction(() => {
13
+ return Object.keys(localStorage).some((storageKey) => storageKey.includes('translations'));
14
+ });
15
+
16
+ await page.evaluate(() => {
17
+ const key = Object.keys(localStorage).find((storageKey) => storageKey.includes('translations'));
18
+
19
+ if (!key) {
20
+ throw new Error('Translations cache not found');
21
+ }
22
+
23
+ const stored = JSON.parse(localStorage.getItem(key) as string) as { translations: Record<string, string> };
24
+ delete stored.translations['words.validity'];
25
+ localStorage.setItem(key, JSON.stringify(stored));
26
+ });
27
+ await page.reload();
28
+
29
+ await page.getByLabel('Created words', { exact: true }).click();
30
+ await expect(Lib.getOpenModal(page).getByRole('button', { name: 'Validity', exact: true })).toBeVisible();
31
+ });
32
+ });
@@ -0,0 +1,86 @@
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
+ });
@@ -0,0 +1,75 @@
1
+ import { expect, type Page, 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: 'On' | 'Off') {
63
+ await Lib.getSettingsButton(page).click();
64
+ await expect(Lib.getOpenModal(page)).toBeVisible();
65
+ await Lib.getSettingOption(page, 'Highlight unreachable cells', value).check();
66
+ await Lib.closeModal(page);
67
+ }
68
+
69
+ async function expectUnreachable(page: Page, x: number, y: number) {
70
+ await expect(Lib.getBoardTile(page, x, y).locator('..')).toHaveClass(/unreachable/);
71
+ }
72
+
73
+ async function expectReachable(page: Page, x: number, y: number) {
74
+ await expect(Lib.getBoardTile(page, x, y).locator('..')).not.toHaveClass(/unreachable/);
75
+ }
@@ -63,6 +63,22 @@ test.describe('#91 - Hovering remaining tile highlights all same tiles on the bo
63
63
  await expect(page.getByTestId('remaining-tile-a').locator('[role="mark"]')).not.toHaveCount(0);
64
64
  await Lib.expectTileHighlighted(Lib.getRackTile(page, 0));
65
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
+ });
66
82
  });
67
83
 
68
84
  async function openRemainingTilesModal(page: Page) {
@@ -70,6 +86,12 @@ async function openRemainingTilesModal(page: Page) {
70
86
  await expect(Lib.getOpenModal(page)).toBeVisible();
71
87
  }
72
88
 
89
+ async function openRemainingTilesModalFromMenu(page: Page) {
90
+ await page.getByRole('button', { name: 'Menu', exact: true }).click();
91
+ await Lib.getOpenModal(page).getByLabel('Remaining tiles', { exact: true }).click();
92
+ await expect(page.getByTestId('remaining-tile-a')).toBeVisible();
93
+ }
94
+
73
95
  async function hoverRemainingTile(page: Page, character: string) {
74
96
  const testId = character === ' ' ? 'remaining-tile-blank' : `remaining-tile-${character}`;
75
97
  await page.getByTestId(testId).hover();
@@ -0,0 +1,21 @@
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
+ });
@@ -0,0 +1,170 @@
1
+ import { expect, type Page, 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
+ }
@@ -0,0 +1,36 @@
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
+ });
@@ -0,0 +1,42 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ import * as Lib from '../lib';
4
+
5
+ test.describe('Words empty state', () => {
6
+ test('shows an empty state and no search input when the board holds no words', async ({ page }) => {
7
+ await Lib.visitIndex(page);
8
+ await Lib.openWordsModal(page);
9
+ const modal = Lib.getOpenModal(page);
10
+
11
+ await expect(modal.getByText('No created words - the board is empty.', { exact: true })).toBeVisible();
12
+ await expect(Lib.getWordsFilterInput(page)).toHaveCount(0);
13
+ await expect(Lib.getWord(page, 0)).toHaveCount(0);
14
+ });
15
+
16
+ test('replaces the empty state with the table once a word exists', async ({ page }) => {
17
+ await Lib.visitIndex(page);
18
+ await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
19
+ await Lib.openWordsModal(page);
20
+ const modal = Lib.getOpenModal(page);
21
+
22
+ await expect(modal.getByText('No created words - the board is empty.', { exact: true })).toHaveCount(0);
23
+ await Lib.assertWord(page, 0, 'cat');
24
+ await expect(Lib.getWordsFilterInput(page)).toBeVisible();
25
+ });
26
+
27
+ test('returns to the empty state when the last word is removed from the board', async ({ page }) => {
28
+ await Lib.visitIndex(page);
29
+ await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
30
+ await Lib.openWordsModal(page);
31
+ await Lib.assertWord(page, 0, 'cat');
32
+
33
+ for (const x of [3, 4, 5]) {
34
+ await Lib.getBoardTile(page, x, 3).focus();
35
+ await page.keyboard.press('Delete');
36
+ }
37
+
38
+ await expect(
39
+ Lib.getOpenModal(page).getByText('No created words - the board is empty.', { exact: true }),
40
+ ).toBeVisible();
41
+ });
42
+ });