feat(ui): vs-AI comms trim, overlay confirm button, recall-to-slot drag
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
- vs-AI: the comms hub drops the Chat tab (Dictionary only); a finished AI game has no comms at all, so its 💬 history-header entry is hidden too. - Confirm-move ✅: redone as an absolute overlay pinned to the right edge (its right edge sits under the preview caption), so the rack keeps a fixed tile size — this reverts the tile-shrink that resized the letters at 6 tiles. - Recall: dragging a placed tile back to the rack now drops it at the slot the pointer is over (drag-to-position, a gap opens), instead of snapping to its original slot; double-tap still recalls to the origin. New pure recallToSlot. Tests: placement recallToSlot units; e2e recall-to-position; vs-AI comms e2e updated. Docs: UI_DESIGN + FUNCTIONAL (+ru).
This commit is contained in:
@@ -284,6 +284,28 @@ test('a placed tile drags from one board cell to another (relocation)', async ({
|
||||
expect(to).not.toBe(from);
|
||||
});
|
||||
|
||||
test('a placed tile drags back to a chosen rack slot (recall-to-position)', async ({ page }) => {
|
||||
await openGame(page);
|
||||
// Place the first rack tile (slot 0) on the board, remembering its letter.
|
||||
await page.locator('.rack .tile').first().click();
|
||||
const placed = (await page.locator('.rack .tile .letter').first().textContent()) ?? '';
|
||||
await page.locator('[data-cell]:not(.filled)').nth(30).click();
|
||||
const pending = page.locator('[data-cell].pending');
|
||||
await expect(pending).toHaveCount(1);
|
||||
|
||||
// Drag it from the board back onto a later rack slot (the 4th visible tile), not its origin.
|
||||
const fb = await pending.first().boundingBox();
|
||||
const slot = await page.locator('[data-rack] .tile').nth(3).boundingBox();
|
||||
await page.mouse.move(fb!.x + fb!.width / 2, fb!.y + fb!.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(slot!.x + 2, slot!.y + slot!.height / 2, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
|
||||
// Recalled (no pending) and reinserted at the drop slot — slot 3, not its origin slot 0.
|
||||
await expect(pending).toHaveCount(0);
|
||||
await expect(page.locator('[data-rack] .tile .letter').nth(3)).toHaveText(placed);
|
||||
});
|
||||
|
||||
test('comms hub: chat and dictionary share a screen, back returns to the game', async ({ page }) => {
|
||||
await openGame(page);
|
||||
await page.locator('.scoreboard').click(); // open the history
|
||||
|
||||
+13
-11
@@ -50,18 +50,20 @@ test('AI game: 🤖 opponent, no wait, chat disabled, dictionary still works', a
|
||||
await expect(page.locator('.scoreboard').getByText('🤖')).toBeVisible();
|
||||
await expect(page.getByText(/Searching for opponent/)).toHaveCount(0);
|
||||
|
||||
// Chat is disabled (the message field), while the dictionary word-check stays usable.
|
||||
// An AI game has no chat at all: the comms hub drops the Chat tab and lands on the Dictionary
|
||||
// alone, which stays usable.
|
||||
await page.locator('.scoreboard').click(); // open the history
|
||||
await page.getByRole('button', { name: 'Chat' }).click(); // 💬 -> comms hub
|
||||
await page.getByRole('button', { name: 'Chat' }).click(); // 💬 (the history-header entry) -> comms hub
|
||||
await expect(page).toHaveURL(/\/game\/.+\/chat$/);
|
||||
await expect(page.locator('.chat input')).toBeDisabled();
|
||||
await page.getByRole('button', { name: 'Dictionary' }).click();
|
||||
await expect(page.locator('.check input')).toBeEnabled();
|
||||
await expect(page.getByRole('button', { name: 'Chat' })).toHaveCount(0); // no Chat tab
|
||||
await expect(page.locator('.chat input')).toHaveCount(0); // the chat body never mounts
|
||||
await expect(page.locator('.check input')).toBeEnabled(); // the Dictionary is the only surface
|
||||
});
|
||||
|
||||
// GCG export is pointless for a practice AI game, so the finished AI game hides the 📤 export
|
||||
// (the comms hub stays). Start an AI game, resign it, reopen the history and check the header.
|
||||
test('AI game: no GCG export offered after it ends', async ({ page }) => {
|
||||
// GCG export is pointless for a practice AI game, and a finished AI game has no comms at all
|
||||
// (no chat, and the dictionary closes with the game), so both the 📤 export and the 💬 comms
|
||||
// entry are gone. Start an AI game, resign it, reopen the history and check the header.
|
||||
test('AI game: after it ends, no GCG export and no comms entry', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: /guest/i }).click();
|
||||
await page.getByRole('button', { name: /🎲/ }).click();
|
||||
@@ -75,11 +77,11 @@ test('AI game: no GCG export offered after it ends', async ({ page }) => {
|
||||
await page.locator('button.danger').click();
|
||||
await expect(page.locator('.status .over')).toBeVisible();
|
||||
|
||||
// Reopen the history (resigning auto-closed it): the finished AI game offers no GCG export,
|
||||
// while the comms hub stays reachable.
|
||||
// Reopen the history (resigning auto-closed it): the finished AI game offers no GCG export and
|
||||
// no comms entry at all.
|
||||
await page.locator('.scoreboard').click();
|
||||
await expect(page.getByRole('button', { name: 'Export GCG' })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: 'Chat' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Chat' })).toHaveCount(0);
|
||||
});
|
||||
|
||||
// The opponent_joined push is best-effort and never replayed, so a join that lands while the live
|
||||
|
||||
Reference in New Issue
Block a user