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
@@ -0,0 +1,179 @@
1
+ import { expect, type Locator, type Page, test } from '@playwright/test';
2
+
3
+ import * as Lib from '../lib';
4
+
5
+ test.describe('Created words interactions', () => {
6
+ test.describe('compact, non-touch', () => {
7
+ test.use({ hasTouch: false, viewport: { width: 800, height: 900 } });
8
+
9
+ test('offers a preview button and no insert button', async ({ page }) => {
10
+ await openWords(page);
11
+
12
+ await expect(Lib.getOpenModal(page).getByRole('button', { name: 'Preview', exact: true })).toBeVisible();
13
+ await expect(Lib.getOpenModal(page).getByRole('button', { name: 'Insert', exact: true })).toHaveCount(0);
14
+ });
15
+
16
+ test('marks rows as clickable', expectRowsToBeClickable);
17
+
18
+ test('leaves the highlight untouched while the pointer travels over the list', async ({ page }) => {
19
+ await openWords(page);
20
+ await expect(Lib.getWord(page, 0)).toHaveAttribute('aria-current', 'true');
21
+
22
+ await Lib.hoverWord(page, 3, 5, 'horizontal');
23
+ await expect(Lib.getWord(page, 0)).toHaveAttribute('aria-current', 'true');
24
+ await expect(Lib.getWord(page, 1)).not.toHaveAttribute('aria-current');
25
+
26
+ await Lib.moveMouseAway(page);
27
+ await expect(Lib.getWord(page, 0)).toHaveAttribute('aria-current', 'true');
28
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 3));
29
+ await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 5));
30
+ });
31
+
32
+ test('picks the clicked word and keeps the modal open when it is clicked again', async ({ page }) => {
33
+ await openWords(page);
34
+
35
+ await Lib.getWord(page, 1).click();
36
+ await expect(Lib.getWord(page, 1)).toHaveAttribute('aria-current', 'true');
37
+ await expect(Lib.getWord(page, 0)).not.toHaveAttribute('aria-current');
38
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 5));
39
+
40
+ await Lib.getWord(page, 1).click();
41
+ await Lib.expectModalToStayOpen(page);
42
+ await expect(Lib.getWord(page, 1)).toHaveAttribute('aria-current', 'true');
43
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 5));
44
+ });
45
+
46
+ test('keeps the highlight when previewing and drops it when closing', async ({ page }) => {
47
+ await openWords(page);
48
+ await Lib.getWord(page, 1).click();
49
+
50
+ await Lib.getOpenModal(page).getByRole('button', { name: 'Preview', exact: true }).click();
51
+ await expect(Lib.getModal(page)).toHaveCount(0);
52
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 5));
53
+
54
+ await Lib.openWordsModal(page);
55
+ await Lib.closeModalWithButton(page);
56
+ await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 5));
57
+ });
58
+ });
59
+
60
+ test.describe('compact, touch', () => {
61
+ test.use({ hasTouch: true, viewport: { width: 800, height: 900 } });
62
+
63
+ test('offers a preview button and no insert button', async ({ page }) => {
64
+ await openWords(page);
65
+
66
+ await expect(Lib.getOpenModal(page).getByRole('button', { name: 'Preview', exact: true })).toBeVisible();
67
+ await expect(Lib.getOpenModal(page).getByRole('button', { name: 'Insert', exact: true })).toHaveCount(0);
68
+ });
69
+
70
+ test('marks rows as clickable', expectRowsToBeClickable);
71
+
72
+ test('picks the tapped word and previews it when it is tapped again', expectRepeatedTapToPreview);
73
+
74
+ test('drops the highlight when the modal is closed', expectClosingToDropTheHighlight);
75
+ });
76
+
77
+ test.describe('desktop, non-touch', () => {
78
+ test.use({ hasTouch: false, viewport: { width: 1440, height: 900 } });
79
+
80
+ test('offers no preview or insert button', expectNoFooterButtons);
81
+
82
+ test('does not mark rows as clickable', async ({ page }) => {
83
+ await openWords(page);
84
+
85
+ await Lib.expectRowClickable(Lib.getWord(page, 0), false);
86
+ });
87
+
88
+ test('hands the highlight over to the hovered row', async ({ page }) => {
89
+ await openWords(page);
90
+ const idleBackground = await getBackgroundColor(Lib.getWord(page, 1));
91
+
92
+ await Lib.getWord(page, 1).click();
93
+ await expect(Lib.getWord(page, 1)).toHaveAttribute('aria-current', 'true');
94
+ expect(await getBackgroundColor(Lib.getWord(page, 1))).not.toBe(idleBackground);
95
+
96
+ await Lib.hoverWord(page, 3, 3, 'horizontal');
97
+ await expect(Lib.getWord(page, 0)).toHaveAttribute('aria-current', 'true');
98
+ await expect(Lib.getWord(page, 1)).not.toHaveAttribute('aria-current');
99
+ await expect(Lib.getWord(page, 1)).toBeFocused();
100
+ expect(await getBackgroundColor(Lib.getWord(page, 1))).toBe(idleBackground);
101
+
102
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 3));
103
+ await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 5));
104
+ });
105
+
106
+ test('ignores clicks and drops the highlight when the pointer leaves the list', async ({ page }) => {
107
+ await openWords(page);
108
+
109
+ await Lib.getWord(page, 1).click();
110
+ await expect(Lib.getWord(page, 1)).toHaveAttribute('aria-current', 'true');
111
+
112
+ await Lib.getWord(page, 1).click();
113
+ await Lib.expectModalToStayOpen(page);
114
+
115
+ await Lib.moveMouseAway(page);
116
+ await expect(Lib.getWord(page, 1)).not.toHaveAttribute('aria-current');
117
+ await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 5));
118
+ });
119
+ });
120
+
121
+ test.describe('desktop, touch', () => {
122
+ test.use({ hasTouch: true, viewport: { width: 1440, height: 900 } });
123
+
124
+ test('offers no preview or insert button', expectNoFooterButtons);
125
+
126
+ test('marks rows as clickable', expectRowsToBeClickable);
127
+
128
+ test('picks the tapped word and previews it when it is tapped again', expectRepeatedTapToPreview);
129
+
130
+ test('drops the highlight when the modal is closed', expectClosingToDropTheHighlight);
131
+ });
132
+ });
133
+
134
+ async function expectRowsToBeClickable({ page }: { page: Page }): Promise<void> {
135
+ await openWords(page);
136
+
137
+ await Lib.expectRowClickable(Lib.getWord(page, 0), true);
138
+ }
139
+
140
+ async function expectRepeatedTapToPreview({ page }: { page: Page }): Promise<void> {
141
+ await openWords(page);
142
+
143
+ await Lib.getWord(page, 1).click();
144
+ await expect(Lib.getWord(page, 1)).toHaveAttribute('aria-current', 'true');
145
+ await Lib.expectModalToStayOpen(page);
146
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 5));
147
+
148
+ await Lib.getWord(page, 1).click();
149
+ await expect(Lib.getModal(page)).toHaveCount(0);
150
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 5));
151
+ }
152
+
153
+ async function expectClosingToDropTheHighlight({ page }: { page: Page }): Promise<void> {
154
+ await openWords(page);
155
+ await Lib.getWord(page, 1).click();
156
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 5));
157
+
158
+ await Lib.closeModalWithButton(page);
159
+ await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 5));
160
+ }
161
+
162
+ async function expectNoFooterButtons({ page }: { page: Page }): Promise<void> {
163
+ await openWords(page);
164
+
165
+ await expect(Lib.getOpenModal(page).getByRole('button', { name: 'Preview', exact: true })).toHaveCount(0);
166
+ await expect(Lib.getOpenModal(page).getByRole('button', { name: 'Insert', exact: true })).toHaveCount(0);
167
+ }
168
+
169
+ async function openWords(page: Page): Promise<void> {
170
+ await Lib.preferKeyboardInput(page);
171
+ await Lib.visitIndex(page);
172
+ await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
173
+ await Lib.typeBoard(page, 'dog', 'horizontal', { x: 3, y: 5 });
174
+ await Lib.openWordsModal(page);
175
+ }
176
+
177
+ function getBackgroundColor(row: Locator): Promise<string> {
178
+ return row.evaluate((element) => globalThis.getComputedStyle(element).backgroundColor);
179
+ }
@@ -0,0 +1,140 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ import * as Lib from '../lib';
4
+
5
+ test.describe('Words preview', () => {
6
+ test.use({ hasTouch: true, viewport: { width: 800, height: 900 } });
7
+
8
+ test.beforeEach(async ({ page }) => {
9
+ await Lib.preferKeyboardInput(page);
10
+ });
11
+
12
+ test('selects the first word on open and previews the selected word on the board', async ({ page }) => {
13
+ await Lib.visitIndex(page);
14
+ await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
15
+ await Lib.typeBoard(page, 'dog', 'horizontal', { x: 3, y: 5 });
16
+ await Lib.openWordsModal(page);
17
+
18
+ await expect(Lib.getWord(page, 0)).toHaveAttribute('aria-current', 'true');
19
+
20
+ await Lib.getWord(page, 1).click();
21
+ await expect(Lib.getWord(page, 1)).toHaveAttribute('aria-current', 'true');
22
+ await expect(Lib.getWord(page, 0)).not.toHaveAttribute('aria-current');
23
+
24
+ await Lib.getOpenModal(page).getByRole('button', { name: 'Preview', exact: true }).click();
25
+ await expect(Lib.getOpenModal(page)).toHaveCount(0);
26
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 5));
27
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 4, 5));
28
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 5, 5));
29
+ await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 3));
30
+ });
31
+
32
+ test('highlights either the result candidate or the selected word, never both', async ({ page }) => {
33
+ await Lib.visitIndex(page);
34
+ await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
35
+ await Lib.typeRack(page, 's');
36
+ await Lib.solve(page);
37
+
38
+ await Lib.openWordsModal(page);
39
+ await Lib.getOpenModal(page).getByRole('button', { name: 'Preview', exact: true }).click();
40
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 3));
41
+
42
+ await page.getByRole('button', { name: 'Results', exact: true }).click();
43
+ const result = Lib.getOpenModal(page).getByTestId('result').first();
44
+ await expect(result).not.toHaveAttribute('aria-current');
45
+ await result.click();
46
+ await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 3));
47
+ });
48
+
49
+ test('clears the highlights when the layout breakpoint changes', async ({ page }) => {
50
+ await Lib.visitIndex(page);
51
+ await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
52
+ await Lib.openWordsModal(page);
53
+ await Lib.getOpenModal(page).getByRole('button', { name: 'Preview', exact: true }).click();
54
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 3));
55
+
56
+ await page.setViewportSize({ width: 1280, height: 900 });
57
+ await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 3));
58
+ });
59
+
60
+ test('drops the highlight when the modal is closed without previewing', async ({ page }) => {
61
+ await Lib.visitIndex(page);
62
+ await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
63
+ await Lib.openWordsModal(page);
64
+ await expect(Lib.getWord(page, 0)).toHaveAttribute('aria-current', 'true');
65
+
66
+ await page.keyboard.press('Escape');
67
+ await expect(Lib.getOpenModal(page)).toHaveCount(0);
68
+ await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 3));
69
+ await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 4, 3));
70
+ await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 5, 3));
71
+ });
72
+
73
+ test('drops the highlight when the language changes', async ({ page }) => {
74
+ await Lib.visitIndex(page);
75
+ await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
76
+ await Lib.openWordsModal(page);
77
+ await Lib.getOpenModal(page).getByRole('button', { name: 'Preview', exact: true }).click();
78
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 3));
79
+
80
+ await Lib.getSettingsButton(page).click();
81
+ await Lib.getSettingOption(page, 'Language', 'Polski').check();
82
+ await Lib.closeModal(page);
83
+
84
+ await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 3));
85
+ });
86
+
87
+ test('drops the selection when the selected word is edited on the board', async ({ page }) => {
88
+ await Lib.visitIndex(page);
89
+ await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
90
+ await Lib.typeBoard(page, 'dog', 'horizontal', { x: 3, y: 5 });
91
+ await Lib.openWordsModal(page);
92
+ await Lib.getWord(page, 1).click();
93
+ await expect(Lib.getWord(page, 1)).toHaveAttribute('aria-current', 'true');
94
+
95
+ await Lib.getBoardTile(page, 3, 5).focus();
96
+ await page.keyboard.type('z');
97
+
98
+ await expect(page.getByTestId('word-3-5-horizontal')).toHaveAttribute('aria-label', 'zog');
99
+ await expect(page.getByTestId('word-3-5-horizontal')).not.toHaveAttribute('aria-current');
100
+ await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 5));
101
+ });
102
+
103
+ test.describe('wide enough not to select the first word', () => {
104
+ test.use({ viewport: { width: 1280, height: 900 } });
105
+
106
+ test('forgets the previewed word when the board is cleared', async ({ page }) => {
107
+ await Lib.visitIndex(page);
108
+ await Lib.typeBoard(page, 'zvq', 'horizontal', { x: 3, y: 3 });
109
+ await Lib.openWordsModal(page);
110
+ await Lib.getWord(page, 0).click();
111
+ await Lib.getWord(page, 0).click();
112
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 3));
113
+
114
+ await page.getByRole('button', { name: 'Clear', exact: true }).click();
115
+ await Lib.typeBoard(page, 'zvq', 'horizontal', { x: 3, y: 3 });
116
+ await Lib.openWordsModal(page);
117
+
118
+ await expect(Lib.getWord(page, 0)).toHaveAttribute('aria-label', 'zvq');
119
+ await expect(Lib.getWord(page, 0)).not.toHaveAttribute('aria-current');
120
+ await Lib.expectTileNotHighlighted(Lib.getBoardTile(page, 3, 3));
121
+ });
122
+ });
123
+
124
+ test.describe('phone', () => {
125
+ test.use({ viewport: { width: 420, height: 900 } });
126
+
127
+ test('previewing from the menu-opened modal returns to the board', async ({ page }) => {
128
+ await Lib.visitIndex(page);
129
+ await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
130
+ await page.getByRole('button', { name: 'Menu', exact: true }).click();
131
+ await Lib.getOpenModal(page).getByLabel('Created words', { exact: true }).click();
132
+
133
+ await expect(Lib.getWord(page, 0)).toHaveAttribute('aria-current', 'true');
134
+
135
+ await page.getByRole('button', { name: 'Preview', exact: true }).click();
136
+ await expect(Lib.getModal(page)).toHaveCount(0);
137
+ await Lib.expectTileHighlighted(Lib.getBoardTile(page, 3, 3));
138
+ });
139
+ });
140
+ });
@@ -0,0 +1,22 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ import * as Lib from '../lib';
4
+
5
+ test.describe('Words table filtering', () => {
6
+ test('groups matching words first and dims the rest', async ({ page }) => {
7
+ await Lib.visitIndex(page);
8
+ await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
9
+ await Lib.typeBoard(page, 'dog', 'horizontal', { x: 3, y: 5 });
10
+ await Lib.openWordsModal(page);
11
+
12
+ await Lib.assertWord(page, 0, 'cat');
13
+ await Lib.assertWord(page, 1, 'dog');
14
+
15
+ await Lib.getWordsFilterInput(page).pressSequentially('d');
16
+
17
+ await Lib.assertWord(page, 0, 'dog');
18
+ await Lib.assertWord(page, 1, 'cat');
19
+ await expect(Lib.getWord(page, 0)).not.toHaveAttribute('aria-hidden');
20
+ await expect(Lib.getWord(page, 1)).toHaveAttribute('aria-hidden', 'true');
21
+ });
22
+ });
@@ -0,0 +1,33 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ import * as Lib from '../lib';
4
+
5
+ test.describe('Words table sorting', () => {
6
+ test('sorts by word, validity, and coordinates with the results-table sort behavior', async ({ page }) => {
7
+ await Lib.visitIndex(page);
8
+ await Lib.typeBoard(page, 'cat', 'horizontal', { x: 3, y: 3 });
9
+ await Lib.typeBoard(page, 'zvq', 'horizontal', { x: 3, y: 5 });
10
+ await Lib.openWordsModal(page);
11
+ const modal = Lib.getOpenModal(page);
12
+
13
+ await Lib.assertWord(page, 0, 'cat');
14
+ await Lib.assertWord(page, 1, 'zvq');
15
+ await expect(Lib.getWord(page, 0)).toContainText('4D');
16
+
17
+ await modal.getByRole('button', { name: 'Word', exact: true }).click();
18
+ await Lib.assertWord(page, 0, 'zvq');
19
+ await Lib.assertWord(page, 1, 'cat');
20
+
21
+ await modal.getByRole('button', { name: 'Validity', exact: true }).click();
22
+ await Lib.assertWord(page, 0, 'cat');
23
+ await Lib.assertWord(page, 1, 'zvq');
24
+
25
+ await modal.getByRole('button', { name: 'Validity', exact: true }).click();
26
+ await Lib.assertWord(page, 0, 'zvq');
27
+ await Lib.assertWord(page, 1, 'cat');
28
+
29
+ await modal.getByRole('button', { name: 'Coordinates', exact: true }).click();
30
+ await Lib.assertWord(page, 0, 'cat');
31
+ await Lib.assertWord(page, 1, 'zvq');
32
+ });
33
+ });
@@ -2,15 +2,21 @@ import { expect, type Locator, type Page } from '@playwright/test';
2
2
  import { type Direction } from '@scrabble-solver/types';
3
3
 
4
4
  import {
5
+ getBoardContainer,
5
6
  getBoardTile,
6
7
  getLoading,
7
8
  getModal,
9
+ getModalResult,
8
10
  getOpenModal,
9
11
  getRackTile,
10
12
  getResult,
13
+ getResultCandidatePicker,
11
14
  getResultsContainer,
15
+ getWord,
12
16
  } from './selectors';
13
17
 
18
+ const MODAL_CLOSE_TRANSITION_TIMEOUT = 500;
19
+
14
20
  interface BoardPosition {
15
21
  x?: number;
16
22
  y?: number;
@@ -20,12 +26,38 @@ export async function visitIndex(page: Page): Promise<void> {
20
26
  await page.goto('/');
21
27
  }
22
28
 
29
+ export async function preferKeyboardInput(page: Page): Promise<void> {
30
+ await page.addInitScript(() => {
31
+ localStorage.setItem('scrabble-solver.settings', JSON.stringify({ inputMode: 'keyboard' }));
32
+ });
33
+ }
34
+
23
35
  export async function closeModal(page: Page): Promise<void> {
24
36
  await expect(getOpenModal(page)).toBeVisible();
25
37
  await page.keyboard.press('Escape');
26
38
  await expect(getModal(page)).toHaveCount(0);
27
39
  }
28
40
 
41
+ export async function openWordsModal(page: Page): Promise<void> {
42
+ await page.getByLabel('Created words', { exact: true }).click();
43
+ await expect(getOpenModal(page)).toBeVisible();
44
+ }
45
+
46
+ export async function openResultsModal(page: Page): Promise<void> {
47
+ await getResultCandidatePicker(page).click();
48
+ await expect(getOpenModal(page)).toBeVisible();
49
+ }
50
+
51
+ export async function closeModalWithButton(page: Page): Promise<void> {
52
+ await getOpenModal(page).getByLabel('Close', { exact: true }).click();
53
+ await expect(getModal(page)).toHaveCount(0);
54
+ }
55
+
56
+ export async function expectModalToStayOpen(page: Page): Promise<void> {
57
+ await page.waitForTimeout(MODAL_CLOSE_TRANSITION_TIMEOUT);
58
+ await expect(getOpenModal(page)).toBeVisible();
59
+ }
60
+
29
61
  export async function typeRack(page: Page, tiles: string, index = 0): Promise<void> {
30
62
  await getRackTile(page, index).focus();
31
63
  await page.keyboard.type(tiles);
@@ -79,16 +111,34 @@ export async function solve(page: Page): Promise<void> {
79
111
  await expect(getLoading(page)).toHaveCount(0);
80
112
  }
81
113
 
82
- /**
83
- * react-window remounts rows during its initial measure pass, and a row
84
- * replaced under an already-hovered cursor never receives a new mouseenter -
85
- * let the list settle before hovering.
86
- */
87
114
  export async function hoverResult(page: Page, index = 0): Promise<void> {
88
- await page.waitForTimeout(100);
115
+ await waitForReactWindowToSettle(page);
89
116
  await getResult(page, index).hover();
90
117
  }
91
118
 
119
+ export async function hoverModalResult(page: Page, index = 0): Promise<void> {
120
+ await waitForReactWindowToSettle(page);
121
+ await getModalResult(page, index).hover();
122
+ }
123
+
124
+ export async function hoverWord(page: Page, x: number, y: number, direction: Direction): Promise<void> {
125
+ await waitForReactWindowToSettle(page);
126
+ await page.getByTestId(`word-${x}-${y}-${direction}`).hover();
127
+ }
128
+
129
+ async function waitForReactWindowToSettle(page: Page) {
130
+ /**
131
+ * react-window remounts rows during its initial measure pass, and a row
132
+ * replaced under an already-hovered cursor never receives a new mouseenter -
133
+ * let the list settle before hovering.
134
+ */
135
+ await page.waitForTimeout(100);
136
+ }
137
+
138
+ export async function assertWord(page: Page, index: number, word: string): Promise<void> {
139
+ await expect(getWord(page, index)).toHaveAttribute('aria-label', word);
140
+ }
141
+
92
142
  export async function assertResult(page: Page, index: number, word: string, points: number): Promise<void> {
93
143
  const result = getResult(page, index);
94
144
 
@@ -97,6 +147,26 @@ export async function assertResult(page: Page, index: number, word: string, poin
97
147
  await expect(result.getByTestId('points')).toHaveText(String(points));
98
148
  }
99
149
 
150
+ export async function expectBoardCharacters(page: Page, characters: string[]): Promise<void> {
151
+ await expect
152
+ .poll(() =>
153
+ getBoardContainer(page)
154
+ .getByRole('textbox', { includeHidden: true })
155
+ .evaluateAll((inputs) => inputs.map((input) => (input as HTMLInputElement).value).filter(Boolean)),
156
+ )
157
+ .toEqual(characters);
158
+ }
159
+
160
+ export async function expectRowClickable(row: Locator, clickable: boolean): Promise<void> {
161
+ const cursor = await row.evaluate((element) => globalThis.getComputedStyle(element).cursor);
162
+
163
+ if (clickable) {
164
+ expect(cursor).toBe('pointer');
165
+ } else {
166
+ expect(cursor).not.toBe('pointer');
167
+ }
168
+ }
169
+
100
170
  export async function expectTileHighlighted(tile: Locator): Promise<void> {
101
171
  await expect(tile.locator('..')).toHaveAttribute('role', 'mark');
102
172
  }
@@ -22,13 +22,19 @@ export function getDictionary(page: Page): Locator {
22
22
  }
23
23
 
24
24
  export function getDictionaryInput(page: Page): Locator {
25
- return getDictionary(page).getByRole('textbox');
25
+ return getDictionary(page).getByRole('textbox', {
26
+ includeHidden: true, // so that the dictionary can be asserted on while modal is opened
27
+ });
26
28
  }
27
29
 
28
30
  export function getDictionaryTitles(page: Page): Locator {
29
31
  return getDictionary(page).locator('h2');
30
32
  }
31
33
 
34
+ export function getModalDictionary(page: Page): Locator {
35
+ return getOpenModal(page).locator('[class*="dictionary"]');
36
+ }
37
+
32
38
  export function getLoading(page: Page): Locator {
33
39
  return page.getByTestId('loading');
34
40
  }
@@ -38,8 +44,11 @@ export function getRackContainer(page: Page): Locator {
38
44
  }
39
45
 
40
46
  export function getRackTile(page: Page, index = 0): Locator {
41
- // includeHidden: true so that rack can be interacted with while modal is opened
42
- return getRackContainer(page).getByRole('textbox', { includeHidden: true }).nth(index);
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);
43
52
  }
44
53
 
45
54
  export function getResultsContainer(page: Page): Locator {
@@ -58,6 +67,34 @@ export function getResult(page: Page, index = 0): Locator {
58
67
  return getResults(page).nth(index);
59
68
  }
60
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
+
61
98
  export function getSettingsButton(page: Page): Locator {
62
99
  return page.getByTestId('settings-button');
63
100
  }
package/lerna.json CHANGED
@@ -6,6 +6,6 @@
6
6
  "access": "public"
7
7
  },
8
8
  "tagVersionPrefix": "",
9
- "version": "2.18.1",
9
+ "version": "2.18.3",
10
10
  "$schema": "node_modules/lerna/schemas/lerna-schema.json"
11
11
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "scrabble-solver",
3
- "version": "2.18.1",
4
- "description": "Scrabble Solver 2 - Free, open-source, cross-platform, multi-language analysis tool for Scrabble, Scrabble Duel, Super Scrabble, Letter League, Crossplay, Literaki, and Kelimelik. Quickly find the top-scoring words using the given board and tiles.",
3
+ "version": "2.18.3",
4
+ "description": "Scrabble Solver 2 - Free, open-source, cross-platform, multi-language analysis tool for Scrabble, Scrabble Duel, Super Scrabble, Letter League, Crossplay, Literaki, Kelimelik, and Wyrazy. Quickly find the top-scoring words using the given board and tiles.",
5
5
  "keywords": [
6
6
  "scrabble",
7
7
  "scrabble-solver",
@@ -17,11 +17,12 @@
17
17
  "crossplay",
18
18
  "literaki",
19
19
  "kelimelik",
20
+ "wyrazy",
20
21
  "board game",
21
22
  "gaddag"
22
23
  ],
23
24
  "engines": {
24
- "bun": ">=1.3",
25
+ "bun": ">=1.4",
25
26
  "node": ">=24"
26
27
  },
27
28
  "bin": {
@@ -42,7 +43,7 @@
42
43
  "release:publish": "bun run version:bump && np --no-cleanup --package-manager bun && lerna publish from-package --yes",
43
44
  "screencast": "start-server-and-test start:app http://localhost:3333 screencast:record",
44
45
  "screencast:record": "bun scripts/record-screencast.ts",
45
- "start": "concurrently -n w: bun:start:*",
46
+ "start": "bun run --parallel --no-orphans 'start:*'",
46
47
  "start:app": "bun run --cwd packages/scrabble-solver start",
47
48
  "start:browser": "wait-on http://localhost:3333 && open-cli http://localhost:3333",
48
49
  "test": "bun run build && bun run test-unit && start-server-and-test start:app http://localhost:3333 test-playwright:run",
@@ -79,23 +80,22 @@
79
80
  "packages/scrabble-solver"
80
81
  ],
81
82
  "dependencies": {
82
- "@scrabble-solver/scrabble-solver": "^2.18.1",
83
- "concurrently": "^10.0.5",
83
+ "@scrabble-solver/scrabble-solver": "^2.18.3",
84
84
  "open-cli": "^9.0.0",
85
85
  "wait-on": "^9.1.0"
86
86
  },
87
87
  "devDependencies": {
88
88
  "@playwright/test": "^1.62.1",
89
- "@types/bun": "^1.3.14",
89
+ "@types/bun": "^1.4.0",
90
90
  "@types/node": "^26.2.0",
91
91
  "eslint-import-resolver-typescript": "^4.4.5",
92
92
  "eslint-plugin-import": "^2.32.0",
93
93
  "eslint-plugin-playwright": "^2.11.0",
94
94
  "eslint-plugin-react": "^7.37.5",
95
- "lerna": "^10.0.0",
95
+ "lerna": "^10.0.1",
96
96
  "np": "^12.0.1",
97
- "oxfmt": "^0.63.0",
98
- "oxlint": "^1.78.0",
97
+ "oxfmt": "^0.64.0",
98
+ "oxlint": "^1.79.0",
99
99
  "oxlint-tsgolint": "^7.0.2001",
100
100
  "rimraf": "^6.1.3",
101
101
  "start-server-and-test": "^3.0.12",
@@ -5,3 +5,4 @@ export * from './literaki';
5
5
  export * from './scrabble';
6
6
  export * from './scrabbleDuel';
7
7
  export * from './superScrabble';
8
+ export * from './wyrazy';
@@ -21,3 +21,4 @@ __exportStar(require("./literaki"), exports);
21
21
  __exportStar(require("./scrabble"), exports);
22
22
  __exportStar(require("./scrabbleDuel"), exports);
23
23
  __exportStar(require("./superScrabble"), exports);
24
+ __exportStar(require("./wyrazy"), exports);
@@ -0,0 +1,19 @@
1
+ import { Game } from '@scrabble-solver/types';
2
+ export declare const wyrazy: {
3
+ bingo: {
4
+ score: number;
5
+ };
6
+ blankScore: number;
7
+ blanksCount: number;
8
+ boardHeight: number;
9
+ boardWidth: number;
10
+ game: Game;
11
+ name: string;
12
+ rackSize: number;
13
+ bonuses: {
14
+ multiplier: number;
15
+ type: string;
16
+ x: number;
17
+ y: number;
18
+ }[];
19
+ };