Stage 17: test-contour verification & defect fixes #19
@@ -165,12 +165,14 @@ test('link account: the Telegram web sign-in control is offered in a browser', a
|
|||||||
await expect(page.getByRole('button', { name: 'Link Telegram' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Link Telegram' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('chat send and nudge are icon buttons', async ({ page }) => {
|
test('chat: the message field shows on your turn, the nudge replaces it otherwise', async ({ page }) => {
|
||||||
await loginLobby(page);
|
await loginLobby(page);
|
||||||
await page.getByRole('button', { name: /Ann/ }).click();
|
await page.getByRole('button', { name: /Ann/ }).click(); // g1: your turn
|
||||||
await page.locator('.burger').first().click();
|
await page.locator('.burger').first().click();
|
||||||
await page.getByRole('button', { name: 'Chat' }).click();
|
await page.getByRole('button', { name: 'Chat' }).click();
|
||||||
// Icon-only controls expose their action through the aria-label.
|
// On your turn the message field + Send are shown and the nudge is hidden (Stage 17);
|
||||||
|
// chat and nudge are mutually exclusive by turn. Icon-only controls expose their action
|
||||||
|
// through the aria-label.
|
||||||
await expect(page.getByRole('button', { name: 'Send' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Send' })).toBeVisible();
|
||||||
await expect(page.getByRole('button', { name: 'Nudge' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Nudge' })).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -250,6 +250,9 @@
|
|||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
background: var(--cell-bg);
|
background: var(--cell-bg);
|
||||||
color: var(--prem-text);
|
color: var(--prem-text);
|
||||||
|
/* No mobile tap flash on a cell tap (parity with the web click; the only intentional
|
||||||
|
cell animation is the last-word .flash highlight). */
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
|
|||||||
+28
-12
@@ -6,16 +6,20 @@
|
|||||||
messages,
|
messages,
|
||||||
myId,
|
myId,
|
||||||
busy,
|
busy,
|
||||||
canNudge = true,
|
myTurn = false,
|
||||||
|
nudgeOnCooldown = false,
|
||||||
onsend,
|
onsend,
|
||||||
onnudge,
|
onnudge,
|
||||||
}: {
|
}: {
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
myId: string;
|
myId: string;
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
// Nudging only makes sense while waiting on the opponent; it is disabled on the
|
// Chat and nudge are mutually exclusive by turn (Stage 17): on the player's own turn the
|
||||||
// player's own turn (there is no one to hurry along).
|
// message field + send are shown (and nudging makes no sense — there is no one to
|
||||||
canNudge?: boolean;
|
// hurry); on the opponent's turn only the nudge button shows. While the hourly nudge
|
||||||
|
// cooldown is active the nudge is disabled with an "awaiting reply" caption.
|
||||||
|
myTurn?: boolean;
|
||||||
|
nudgeOnCooldown?: boolean;
|
||||||
onsend: (text: string) => void;
|
onsend: (text: string) => void;
|
||||||
onnudge: () => void;
|
onnudge: () => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
@@ -44,14 +48,18 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<input
|
{#if myTurn}
|
||||||
maxlength="60"
|
<input
|
||||||
placeholder={t('chat.placeholder')}
|
maxlength="60"
|
||||||
bind:value={text}
|
placeholder={t('chat.placeholder')}
|
||||||
onkeydown={(e) => e.key === 'Enter' && send()}
|
bind:value={text}
|
||||||
/>
|
onkeydown={(e) => e.key === 'Enter' && send()}
|
||||||
<button class="iconbtn" onclick={send} disabled={busy} aria-label={t('chat.send')}>⬆️</button>
|
/>
|
||||||
<button class="iconbtn" onclick={onnudge} disabled={busy || !canNudge} aria-label={t('chat.nudgeAction')}>🛎️</button>
|
<button class="iconbtn" onclick={send} disabled={busy} aria-label={t('chat.send')}>⬆️</button>
|
||||||
|
{:else}
|
||||||
|
{#if nudgeOnCooldown}<span class="cooldown">{t('chat.awaitingReply')}</span>{/if}
|
||||||
|
<button class="iconbtn" onclick={onnudge} disabled={busy || nudgeOnCooldown} aria-label={t('chat.nudgeAction')}>🛎️</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -99,6 +107,14 @@
|
|||||||
.input {
|
.input {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
/* The cooldown caption sits to the left of the disabled nudge button. */
|
||||||
|
.cooldown {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
.input input {
|
.input input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -760,7 +760,7 @@
|
|||||||
|
|
||||||
{#if panel === 'chat'}
|
{#if panel === 'chat'}
|
||||||
<Modal title={t('game.chat')} onclose={() => (panel = 'none')}>
|
<Modal title={t('game.chat')} onclose={() => (panel = 'none')}>
|
||||||
<Chat {messages} myId={app.session?.userId ?? ''} {busy} canNudge={!isMyTurn && !nudgeOnCooldown} onsend={sendChat} onnudge={nudge} />
|
<Chat {messages} myId={app.session?.userId ?? ''} {busy} myTurn={isMyTurn} {nudgeOnCooldown} onsend={sendChat} onnudge={nudge} />
|
||||||
</Modal>
|
</Modal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const en = {
|
|||||||
'new.title': 'New game',
|
'new.title': 'New game',
|
||||||
'new.subtitle': 'Auto-match with another player',
|
'new.subtitle': 'Auto-match with another player',
|
||||||
'new.english': 'Scrabble',
|
'new.english': 'Scrabble',
|
||||||
'new.russian': 'Scrabble',
|
'new.russian': 'Скрэббл',
|
||||||
'new.erudit': 'Erudite',
|
'new.erudit': 'Erudite',
|
||||||
'new.find': 'Find a game',
|
'new.find': 'Find a game',
|
||||||
'new.searching': 'Looking for an opponent…',
|
'new.searching': 'Looking for an opponent…',
|
||||||
@@ -96,6 +96,7 @@ export const en = {
|
|||||||
'chat.send': 'Send',
|
'chat.send': 'Send',
|
||||||
'chat.nudge': 'Waiting for your move!',
|
'chat.nudge': 'Waiting for your move!',
|
||||||
'chat.nudgeAction': 'Nudge',
|
'chat.nudgeAction': 'Nudge',
|
||||||
|
'chat.awaitingReply': "Waiting for the opponent's reply",
|
||||||
'chat.empty': 'No messages yet.',
|
'chat.empty': 'No messages yet.',
|
||||||
'chat.nudged': '{name} nudged you',
|
'chat.nudged': '{name} nudged you',
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const ru: Record<MessageKey, string> = {
|
|||||||
|
|
||||||
'new.title': 'Новая игра',
|
'new.title': 'Новая игра',
|
||||||
'new.subtitle': 'Автоподбор соперника',
|
'new.subtitle': 'Автоподбор соперника',
|
||||||
'new.english': 'Скрэббл',
|
'new.english': 'Scrabble',
|
||||||
'new.russian': 'Скрэббл',
|
'new.russian': 'Скрэббл',
|
||||||
'new.erudit': 'Эрудит',
|
'new.erudit': 'Эрудит',
|
||||||
'new.find': 'Найти игру',
|
'new.find': 'Найти игру',
|
||||||
@@ -97,6 +97,7 @@ export const ru: Record<MessageKey, string> = {
|
|||||||
'chat.send': 'Отправить',
|
'chat.send': 'Отправить',
|
||||||
'chat.nudge': 'Жду вашего хода!',
|
'chat.nudge': 'Жду вашего хода!',
|
||||||
'chat.nudgeAction': 'Поторопить',
|
'chat.nudgeAction': 'Поторопить',
|
||||||
|
'chat.awaitingReply': 'Ждём реакцию соперника',
|
||||||
'chat.empty': 'Сообщений пока нет.',
|
'chat.empty': 'Сообщений пока нет.',
|
||||||
'chat.nudged': '{name} торопит вас',
|
'chat.nudged': '{name} торопит вас',
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ export interface VariantOption {
|
|||||||
label: MessageKey;
|
label: MessageKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ALL_VARIANTS lists every variant in display order. The labels are display names, not
|
// ALL_VARIANTS lists every variant in display order. The labels are display names keyed by
|
||||||
// language names: both Scrabble variants render as "Scrabble"/"Скрэббл" and Erudit as
|
// the game's alphabet, not the interface language: the English-alphabet game is always
|
||||||
// "Erudite"/"Эрудит" (Stage 17) — the offered list is language-gated, so within one
|
// "Scrabble" and the Russian-alphabet Scrabble always "Скрэббл" (both unlocalized, so the
|
||||||
// language the names stay distinct.
|
// two never collide whatever the UI language); Erudit is localized "Erudite"/"Эрудит"
|
||||||
|
// (Stage 17).
|
||||||
export const ALL_VARIANTS: VariantOption[] = [
|
export const ALL_VARIANTS: VariantOption[] = [
|
||||||
{ id: 'english', label: 'new.english' },
|
{ id: 'english', label: 'new.english' },
|
||||||
{ id: 'russian_scrabble', label: 'new.russian' },
|
{ id: 'russian_scrabble', label: 'new.russian' },
|
||||||
|
|||||||
@@ -166,8 +166,6 @@
|
|||||||
<div class="name">{p.displayName}</div>
|
<div class="name">{p.displayName}</div>
|
||||||
{#if p.isGuest}<span class="badge">{t('profile.guest')}</span>{/if}
|
{#if p.isGuest}<span class="badge">{t('profile.guest')}</span>{/if}
|
||||||
|
|
||||||
<div class="hintbal"><span>{t('profile.hintBalance')}</span><b>{p.hintBalance}</b></div>
|
|
||||||
|
|
||||||
{#if p.isGuest}
|
{#if p.isGuest}
|
||||||
<p class="muted">{t('profile.guestLocked')}</p>
|
<p class="muted">{t('profile.guestLocked')}</p>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -284,15 +282,6 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
.hintbal {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
.hintbal b {
|
|
||||||
color: var(--text);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.muted {
|
.muted {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user