fix: 按审核结论补强短语实现与测试 (#11)
- adjustRange 改变范围时同时清空短语链接与已加载条目:调整端点后不再声称「已保存」, 已输入文本保留,保存按当前范围确定的身份写入(可能更新另一条,但绝不重复) - 新增 TestWordKeysNeverContainSpaces:显式锁定「词片段不含空白」这一不变量, termKind/termWordCount 完全依赖它,此前只有间接覆盖 - e2e 断言超时提高到 15s:11 个用例共用开发服务器并行运行时,首屏模块加载可能超过 默认 5s 造成偶发失败(产品行为未变,连续两次并行全量运行均通过)
This commit was merged in pull request #30.
This commit is contained in:
@@ -8,6 +8,9 @@ export default defineConfig({
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: 0,
|
||||
reporter: 'list',
|
||||
// Every spec loads the whole SPA from the shared dev server, so a slow first module graph
|
||||
// under parallel workers must not fail an assertion that the app itself would pass.
|
||||
expect: { timeout: 15000 },
|
||||
use: { baseURL: 'http://127.0.0.1:5173', headless: true, trace: 'off' },
|
||||
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'], channel: 'chrome' } }],
|
||||
webServer: { command: 'npm run dev -- --host 127.0.0.1', url: 'http://127.0.0.1:5173', reuseExistingServer: !process.env.CI },
|
||||
|
||||
@@ -131,6 +131,21 @@ describe('phrase selection and panel', () => {
|
||||
expect(view.get('[data-testid="phrase-range"]').text()).toContain('已保存')
|
||||
})
|
||||
|
||||
it('stops claiming a stored phrase once its range is adjusted', async () => {
|
||||
const span: PhraseSpan = { id: 7, status: 'new', wordCount: 3, startToken: 2, endToken: 7 }
|
||||
const { view } = await open({ phrases: [span] })
|
||||
await view.findAll('.reader-word').find(item => item.text() === 'small')!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(view.get('[data-testid="phrase-range"]').text()).toContain('已保存')
|
||||
await view.get('[data-testid="range-end-right"]').trigger('click')
|
||||
await flushPromises()
|
||||
// The adjusted range is another identity, so the panel no longer says it is stored, while
|
||||
// the text the learner already had stays in the form.
|
||||
expect(view.get('.lookup-panel').text()).toContain('新词条')
|
||||
expect(view.get('.lookup-panel').text()).not.toContain('已保存')
|
||||
expect((view.get('#term-definition').element as HTMLTextAreaElement).value).toBe('一小步')
|
||||
})
|
||||
|
||||
it('refuses a range longer than the phrase limit and keeps the panel closed', async () => {
|
||||
// One word more than a phrase may hold.
|
||||
const longTokens: ReaderToken[] = []
|
||||
|
||||
@@ -289,7 +289,14 @@ export function useReaderLookup(chapter: Ref<ChapterDetail | null>) {
|
||||
const currentChapter = chapter.value
|
||||
if (!current || !currentChapter) return
|
||||
const next = adjustTokenRange(tokens.value, currentChapter.originalText ?? '', current, edge, direction)
|
||||
if (next) range.value = next
|
||||
if (!next) return
|
||||
// The adjusted range is a different identity, so the panel must stop claiming that the
|
||||
// stored entry is what is on screen: both the phrase link and the entry the form was loaded
|
||||
// from are dropped. The typed text stays, and saving it writes the phrase that matches the
|
||||
// current range (which may update another entry, never two).
|
||||
rangeTermId.value = null
|
||||
savedTermId.value = null
|
||||
range.value = next
|
||||
}
|
||||
|
||||
/** True when the panel is editing an entry that is already stored. */
|
||||
|
||||
@@ -118,6 +118,35 @@ func TestPhraseIdentityRules(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWordKeysNeverContainSpaces locks the premise the derived kind relies on: a word token
|
||||
// never contains a space, so the identity key alone tells a word from a phrase and no column is
|
||||
// needed for it.
|
||||
func TestWordKeysNeverContainSpaces(t *testing.T) {
|
||||
samples := []string{phraseFixtureText, "a b\tc\r\nd", "don’t stop-BELIEVING.", "🙂 café e\u0301 42"}
|
||||
checked := 0
|
||||
for _, sample := range samples {
|
||||
for _, token := range Tokenize(sample) {
|
||||
if token.Kind != "word" {
|
||||
continue
|
||||
}
|
||||
checked++
|
||||
if strings.ContainsAny(normalizeWord(token.Text), " \t\r\n") {
|
||||
t.Fatalf("word key %q contains whitespace", normalizeWord(token.Text))
|
||||
}
|
||||
}
|
||||
}
|
||||
if checked < 8 {
|
||||
t.Fatalf("expected several word tokens, checked %d", checked)
|
||||
}
|
||||
// The derived kind therefore agrees with the shape of the key in both directions.
|
||||
if termKind("a small step") != termKindPhrase || termWordCount("a small step") != 3 {
|
||||
t.Fatal("a key with spaces must be a three-word phrase")
|
||||
}
|
||||
if termKind("curiosity") != termKindWord || termWordCount("curiosity") != 1 {
|
||||
t.Fatal("a key without spaces must be a word")
|
||||
}
|
||||
}
|
||||
|
||||
func mustWords(t *testing.T, text string) []TextToken {
|
||||
t.Helper()
|
||||
words, err := phraseWords(phraseTokens(text), 0, len([]rune(text)))
|
||||
|
||||
Reference in New Issue
Block a user