Skip to content

Commit 4df78c2

Browse files
committed
fix(web): exclude the exact five-minute boundary from the lease warning
Address review feedback on the question lease UI: - The warning is for less than five minutes remaining, so a lease with exactly five minutes left no longer shows it. - Add a boundary test covering both five minutes and four minutes fifty-nine seconds. - Correct the i18n section of apps/pythinker-web/AGENTS.md: the app ships a single English locale, so the bilingual parity rule described a directory that does not exist.
1 parent 4f847d1 commit 4df78c2

3 files changed

Lines changed: 30 additions & 6 deletions

File tree

apps/pythinker-web/AGENTS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ The browser web UI for Pythinker Code — a peer to the TUI in `apps/pythinker-c
2323
- Shared components go in `src/components/`; reusable logic goes in `src/composables/` with a `use` prefix.
2424
- There is **no auto-import plugin** and **no path alias**`#/` and `@/` are intentionally unused. Write relative imports (`../i18n`, `./config`).
2525

26-
## i18n (normative — keeping locales in sync is manual)
26+
## i18n (normative — the app is English-only)
2727

2828
- Setup: `src/i18n/index.ts`, vue-i18n in Composition mode (`legacy: false`), fallback `en`. The active locale is persisted in `localStorage` under `pythinker-locale`.
29-
- Locale files: `src/i18n/locales/{en,zh}/<namespace>.ts`, each `export default { ... } as const`. New namespaces are registered in `src/i18n/locales/index.ts`.
29+
- **`en` is the only locale.** `src/i18n/locales/` contains exactly one directory, and `locales/index.ts` registers only `en`. Do not add a second locale, and do not "restore parity" with one that does not exist.
30+
- Locale files: `src/i18n/locales/en/<namespace>.ts`, each `export default { ... } as const`. New namespaces are registered in `src/i18n/locales/index.ts`.
3031
- Reference with `const { t } = useI18n()` and `t('namespace.key')` (same form in templates).
31-
- **Adding a key:** add it to **both** `en/<ns>.ts` and `zh/<ns>.ts`. **Adding a namespace:** create the file in both locales **and** register it in `locales/index.ts`.
32-
- There is **no automated missing-key or en/zh parity check**. Keeping the two locales in sync is a manual responsibility — do not leave a key present in only one locale.
32+
- **Adding a key:** add it to `en/<ns>.ts`. **Adding a namespace:** create the file under `en/` **and** register it in `locales/index.ts`.
3333

3434
## Commands
3535

apps/pythinker-web/src/components/QuestionCard.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const leaseWarning = computed(() => {
4040
const expiresAt = Date.parse(props.question.expiresAt);
4141
if (Number.isNaN(expiresAt)) return undefined;
4242
const remainingMs = expiresAt - now.value;
43-
if (remainingMs <= 0 || remainingMs > 5 * 60_000) return undefined;
43+
if (remainingMs <= 0 || remainingMs >= 5 * 60_000) return undefined;
4444
if (remainingMs < 60_000) return t('question.expiresSoonSeconds');
4545
const minutes = remainingMinutes.value;
4646
if (minutes === undefined) return undefined;

apps/pythinker-web/test/question-card-lifecycle.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { mount } from '@vue/test-utils';
22
import { createI18n } from 'vue-i18n';
3-
import { afterEach, describe, expect, it } from 'vitest';
3+
import { afterEach, describe, expect, it, vi } from 'vitest';
44

55
import QuestionCard from '../src/components/QuestionCard.vue';
66
import type { UIQuestion } from '../src/types';
@@ -88,4 +88,28 @@ describe('QuestionCard lifecycle', () => {
8888
expect(soon.find('.qexpires').text()).toBe('Expires in 2 min');
8989
expect(later.find('.qexpires').exists()).toBe(false);
9090
});
91+
92+
it('hides the expiry warning at five minutes and shows it at four minutes fifty-nine seconds', () => {
93+
vi.useFakeTimers();
94+
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
95+
try {
96+
const boundary = mountCard(question(new Date(Date.now() + 5 * 60_000).toISOString()));
97+
const soon = mountCard(question(new Date(Date.now() + 4 * 60_000 + 59_000).toISOString()));
98+
99+
expect(boundary.find('.qexpires').exists()).toBe(false);
100+
expect(soon.find('.qexpires').exists()).toBe(true);
101+
} finally {
102+
vi.useRealTimers();
103+
}
104+
});
105+
106+
it('ignores Enter while minimized', async () => {
107+
const wrapper = mountCard(question(new Date(Date.now() + 20 * 60_000).toISOString()));
108+
109+
document.dispatchEvent(new KeyboardEvent('keydown', { key: '1', bubbles: true }));
110+
await wrapper.find('.qmin').trigger('click');
111+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
112+
113+
expect(wrapper.emitted('answer')).toBeUndefined();
114+
});
91115
});

0 commit comments

Comments
 (0)