mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-20 07:34:38 +00:00
Fix terminal e2e flakiness with a real terminal driver (#17144)
This commit is contained in:
@@ -70,6 +70,8 @@ test("test description", async ({ page, sdk, gotoSession }) => {
|
|||||||
- `openSettings(page)` - Open settings dialog
|
- `openSettings(page)` - Open settings dialog
|
||||||
- `closeDialog(page, dialog)` - Close any dialog
|
- `closeDialog(page, dialog)` - Close any dialog
|
||||||
- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar
|
- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar
|
||||||
|
- `waitTerminalReady(page, { term? })` - Wait for a mounted terminal to connect and finish rendering output
|
||||||
|
- `runTerminal(page, { cmd, token, term?, timeout? })` - Type into the terminal via the browser and wait for rendered output
|
||||||
- `withSession(sdk, title, callback)` - Create temp session
|
- `withSession(sdk, title, callback)` - Create temp session
|
||||||
- `withProject(...)` - Create temp project/workspace
|
- `withProject(...)` - Create temp project/workspace
|
||||||
- `sessionIDFromUrl(url)` - Read session ID from URL
|
- `sessionIDFromUrl(url)` - Read session ID from URL
|
||||||
@@ -167,6 +169,13 @@ await page.keyboard.press(`${modKey}+B`) // Toggle sidebar
|
|||||||
await page.keyboard.press(`${modKey}+Comma`) // Open settings
|
await page.keyboard.press(`${modKey}+Comma`) // Open settings
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Terminal Tests
|
||||||
|
|
||||||
|
- In terminal tests, type through the browser. Do not write to the PTY through the SDK.
|
||||||
|
- Use `waitTerminalReady(page, { term? })` and `runTerminal(page, { cmd, token, term?, timeout? })` from `actions.ts`.
|
||||||
|
- These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles.
|
||||||
|
- Avoid `waitForTimeout` and custom DOM or `data-*` readiness checks.
|
||||||
|
|
||||||
## Writing New Tests
|
## Writing New Tests
|
||||||
|
|
||||||
1. Choose appropriate folder or create new one
|
1. Choose appropriate folder or create new one
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import fs from "node:fs/promises"
|
|||||||
import os from "node:os"
|
import os from "node:os"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { execSync } from "node:child_process"
|
import { execSync } from "node:child_process"
|
||||||
|
import { terminalAttr, type E2EWindow } from "../src/testing/terminal"
|
||||||
import { createSdk, modKey, resolveDirectory, serverUrl } from "./utils"
|
import { createSdk, modKey, resolveDirectory, serverUrl } from "./utils"
|
||||||
import {
|
import {
|
||||||
dropdownMenuTriggerSelector,
|
dropdownMenuTriggerSelector,
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
listItemSelector,
|
listItemSelector,
|
||||||
listItemKeySelector,
|
listItemKeySelector,
|
||||||
listItemKeyStartsWithSelector,
|
listItemKeyStartsWithSelector,
|
||||||
|
terminalSelector,
|
||||||
workspaceItemSelector,
|
workspaceItemSelector,
|
||||||
workspaceMenuTriggerSelector,
|
workspaceMenuTriggerSelector,
|
||||||
} from "./selectors"
|
} from "./selectors"
|
||||||
@@ -28,6 +30,57 @@ export async function defocus(page: Page) {
|
|||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function terminalID(term: Locator) {
|
||||||
|
const id = await term.getAttribute(terminalAttr)
|
||||||
|
if (id) return id
|
||||||
|
throw new Error(`Active terminal missing ${terminalAttr}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function terminalReady(page: Page, term?: Locator) {
|
||||||
|
const next = term ?? page.locator(terminalSelector).first()
|
||||||
|
const id = await terminalID(next)
|
||||||
|
return page.evaluate((id) => {
|
||||||
|
const state = (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[id]
|
||||||
|
return !!state?.connected && (state.settled ?? 0) > 0
|
||||||
|
}, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function terminalHas(page: Page, input: { term?: Locator; token: string }) {
|
||||||
|
const next = input.term ?? page.locator(terminalSelector).first()
|
||||||
|
const id = await terminalID(next)
|
||||||
|
return page.evaluate(
|
||||||
|
(input) => {
|
||||||
|
const state = (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[input.id]
|
||||||
|
return state?.rendered.includes(input.token) ?? false
|
||||||
|
},
|
||||||
|
{ id, token: input.token },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitTerminalReady(page: Page, input?: { term?: Locator; timeout?: number }) {
|
||||||
|
const term = input?.term ?? page.locator(terminalSelector).first()
|
||||||
|
const timeout = input?.timeout ?? 10_000
|
||||||
|
await expect(term).toBeVisible()
|
||||||
|
await expect(term.locator("textarea")).toHaveCount(1)
|
||||||
|
await expect
|
||||||
|
.poll(() => terminalReady(page, term), { timeout })
|
||||||
|
.toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runTerminal(page: Page, input: { cmd: string; token: string; term?: Locator; timeout?: number }) {
|
||||||
|
const term = input.term ?? page.locator(terminalSelector).first()
|
||||||
|
const timeout = input.timeout ?? 10_000
|
||||||
|
await waitTerminalReady(page, { term, timeout })
|
||||||
|
const textarea = term.locator("textarea")
|
||||||
|
await term.click()
|
||||||
|
await expect(textarea).toBeFocused()
|
||||||
|
await page.keyboard.type(input.cmd)
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
await expect
|
||||||
|
.poll(() => terminalHas(page, { term, token: input.token }), { timeout })
|
||||||
|
.toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
export async function openPalette(page: Page) {
|
export async function openPalette(page: Page) {
|
||||||
await defocus(page)
|
await defocus(page)
|
||||||
await page.keyboard.press(`${modKey}+P`)
|
await page.keyboard.press(`${modKey}+P`)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { test as base, expect, type Page } from "@playwright/test"
|
import { test as base, expect, type Page } from "@playwright/test"
|
||||||
|
import type { E2EWindow } from "../src/testing/terminal"
|
||||||
import { cleanupSession, cleanupTestProject, createTestProject, seedProjects, sessionIDFromUrl } from "./actions"
|
import { cleanupSession, cleanupTestProject, createTestProject, seedProjects, sessionIDFromUrl } from "./actions"
|
||||||
import { promptSelector } from "./selectors"
|
import { promptSelector } from "./selectors"
|
||||||
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
|
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
|
||||||
@@ -91,6 +92,14 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|||||||
async function seedStorage(page: Page, input: { directory: string; extra?: string[] }) {
|
async function seedStorage(page: Page, input: { directory: string; extra?: string[] }) {
|
||||||
await seedProjects(page, input)
|
await seedProjects(page, input)
|
||||||
await page.addInitScript(() => {
|
await page.addInitScript(() => {
|
||||||
|
const win = window as E2EWindow
|
||||||
|
win.__opencode_e2e = {
|
||||||
|
...win.__opencode_e2e,
|
||||||
|
terminal: {
|
||||||
|
enabled: true,
|
||||||
|
terminals: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"opencode.global.dat:model",
|
"opencode.global.dat:model",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
|
import { waitTerminalReady } from "../actions"
|
||||||
import { promptSelector, terminalSelector } from "../selectors"
|
import { promptSelector, terminalSelector } from "../selectors"
|
||||||
|
|
||||||
test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
|
test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
|
||||||
@@ -13,7 +14,7 @@ test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
|
|||||||
await prompt.fill("/terminal")
|
await prompt.fill("/terminal")
|
||||||
await expect(slash).toBeVisible()
|
await expect(slash).toBeVisible()
|
||||||
await page.keyboard.press("Enter")
|
await page.keyboard.press("Enter")
|
||||||
await expect(terminal).toBeVisible()
|
await waitTerminalReady(page, { term: terminal })
|
||||||
|
|
||||||
// Terminal panel retries focus (immediate, RAF, 120ms, 240ms) after opening,
|
// Terminal panel retries focus (immediate, RAF, 120ms, 240ms) after opening,
|
||||||
// which can steal focus from the prompt and prevent fill() from triggering
|
// which can steal focus from the prompt and prevent fill() from triggering
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { openSettings, closeDialog, withSession } from "../actions"
|
import { openSettings, closeDialog, waitTerminalReady, withSession } from "../actions"
|
||||||
import { keybindButtonSelector, terminalSelector } from "../selectors"
|
import { keybindButtonSelector, terminalSelector } from "../selectors"
|
||||||
import { modKey } from "../utils"
|
import { modKey } from "../utils"
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ test("changing terminal toggle keybind works", async ({ page, gotoSession }) =>
|
|||||||
await expect(terminal).not.toBeVisible()
|
await expect(terminal).not.toBeVisible()
|
||||||
|
|
||||||
await page.keyboard.press(`${modKey}+Y`)
|
await page.keyboard.press(`${modKey}+Y`)
|
||||||
await expect(terminal).toBeVisible()
|
await waitTerminalReady(page, { term: terminal })
|
||||||
|
|
||||||
await page.keyboard.press(`${modKey}+Y`)
|
await page.keyboard.press(`${modKey}+Y`)
|
||||||
await expect(terminal).not.toBeVisible()
|
await expect(terminal).not.toBeVisible()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
|
import { waitTerminalReady } from "../actions"
|
||||||
import { promptSelector, terminalSelector } from "../selectors"
|
import { promptSelector, terminalSelector } from "../selectors"
|
||||||
import { terminalToggleKey } from "../utils"
|
import { terminalToggleKey } from "../utils"
|
||||||
|
|
||||||
@@ -13,8 +14,7 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes
|
|||||||
await page.keyboard.press(terminalToggleKey)
|
await page.keyboard.press(terminalToggleKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(terminals.first()).toBeVisible()
|
await waitTerminalReady(page, { term: terminals.first() })
|
||||||
await expect(terminals.first().locator("textarea")).toHaveCount(1)
|
|
||||||
await expect(terminals).toHaveCount(1)
|
await expect(terminals).toHaveCount(1)
|
||||||
|
|
||||||
// Ghostty captures a lot of keybinds when focused; move focus back
|
// Ghostty captures a lot of keybinds when focused; move focus back
|
||||||
@@ -24,5 +24,5 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes
|
|||||||
|
|
||||||
await expect(tabs).toHaveCount(2)
|
await expect(tabs).toHaveCount(2)
|
||||||
await expect(terminals).toHaveCount(1)
|
await expect(terminals).toHaveCount(1)
|
||||||
await expect(terminals.first().locator("textarea")).toHaveCount(1)
|
await waitTerminalReady(page, { term: terminals.first() })
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Page } from "@playwright/test"
|
import type { Page } from "@playwright/test"
|
||||||
|
import { runTerminal, waitTerminalReady } from "../actions"
|
||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { terminalSelector } from "../selectors"
|
import { terminalSelector } from "../selectors"
|
||||||
import { terminalToggleKey, workspacePersistKey } from "../utils"
|
import { terminalToggleKey, workspacePersistKey } from "../utils"
|
||||||
@@ -17,16 +18,7 @@ async function open(page: Page) {
|
|||||||
const terminal = page.locator(terminalSelector)
|
const terminal = page.locator(terminalSelector)
|
||||||
const visible = await terminal.isVisible().catch(() => false)
|
const visible = await terminal.isVisible().catch(() => false)
|
||||||
if (!visible) await page.keyboard.press(terminalToggleKey)
|
if (!visible) await page.keyboard.press(terminalToggleKey)
|
||||||
await expect(terminal).toBeVisible()
|
await waitTerminalReady(page, { term: terminal })
|
||||||
await expect(terminal.locator("textarea")).toHaveCount(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run(page: Page, cmd: string) {
|
|
||||||
const terminal = page.locator(terminalSelector)
|
|
||||||
await expect(terminal).toBeVisible()
|
|
||||||
await terminal.click()
|
|
||||||
await page.keyboard.type(cmd)
|
|
||||||
await page.keyboard.press("Enter")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function store(page: Page, key: string) {
|
async function store(page: Page, key: string) {
|
||||||
@@ -56,15 +48,16 @@ test("inactive terminal tab buffers persist across tab switches", async ({ page,
|
|||||||
await gotoSession()
|
await gotoSession()
|
||||||
await open(page)
|
await open(page)
|
||||||
|
|
||||||
await run(page, `echo ${one}`)
|
await runTerminal(page, { cmd: `echo ${one}`, token: one })
|
||||||
|
|
||||||
await page.getByRole("button", { name: /new terminal/i }).click()
|
await page.getByRole("button", { name: /new terminal/i }).click()
|
||||||
await expect(tabs).toHaveCount(2)
|
await expect(tabs).toHaveCount(2)
|
||||||
|
|
||||||
await run(page, `echo ${two}`)
|
await runTerminal(page, { cmd: `echo ${two}`, token: two })
|
||||||
|
|
||||||
await first.click()
|
await first.click()
|
||||||
await expect(first).toHaveAttribute("aria-selected", "true")
|
await expect(first).toHaveAttribute("aria-selected", "true")
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
@@ -76,7 +69,7 @@ test("inactive terminal tab buffers persist across tab switches", async ({ page,
|
|||||||
second: second.includes(two),
|
second: second.includes(two),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ timeout: 30_000 },
|
{ timeout: 5_000 },
|
||||||
)
|
)
|
||||||
.toEqual({ first: false, second: true })
|
.toEqual({ first: false, second: true })
|
||||||
|
|
||||||
@@ -93,7 +86,7 @@ test("inactive terminal tab buffers persist across tab switches", async ({ page,
|
|||||||
second: second.includes(two),
|
second: second.includes(two),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ timeout: 30_000 },
|
{ timeout: 5_000 },
|
||||||
)
|
)
|
||||||
.toEqual({ first: true, second: false })
|
.toEqual({ first: true, second: false })
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
|
import { waitTerminalReady } from "../actions"
|
||||||
import { terminalSelector } from "../selectors"
|
import { terminalSelector } from "../selectors"
|
||||||
import { terminalToggleKey } from "../utils"
|
import { terminalToggleKey } from "../utils"
|
||||||
|
|
||||||
@@ -13,5 +14,5 @@ test("terminal panel can be toggled", async ({ page, gotoSession }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await page.keyboard.press(terminalToggleKey)
|
await page.keyboard.press(terminalToggleKey)
|
||||||
await expect(terminal).toBeVisible()
|
await waitTerminalReady(page, { term: terminal })
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useSDK } from "@/context/sdk"
|
|||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
import { monoFontFamily, useSettings } from "@/context/settings"
|
import { monoFontFamily, useSettings } from "@/context/settings"
|
||||||
import type { LocalPTY } from "@/context/terminal"
|
import type { LocalPTY } from "@/context/terminal"
|
||||||
|
import { terminalAttr, terminalProbe } from "@/testing/terminal"
|
||||||
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
|
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
|
||||||
import { terminalWriter } from "@/utils/terminal-writer"
|
import { terminalWriter } from "@/utils/terminal-writer"
|
||||||
|
|
||||||
@@ -160,6 +161,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
let container!: HTMLDivElement
|
let container!: HTMLDivElement
|
||||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
|
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
|
||||||
const id = local.pty.id
|
const id = local.pty.id
|
||||||
|
const probe = terminalProbe(id)
|
||||||
const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : ""
|
const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : ""
|
||||||
const restoreSize =
|
const restoreSize =
|
||||||
restore &&
|
restore &&
|
||||||
@@ -326,6 +328,9 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
probe.init()
|
||||||
|
cleanups.push(() => probe.drop())
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
const loaded = await loadGhostty()
|
const loaded = await loadGhostty()
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
@@ -353,7 +358,13 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
}
|
}
|
||||||
ghostty = g
|
ghostty = g
|
||||||
term = t
|
term = t
|
||||||
output = terminalWriter((data, done) => t.write(data, done))
|
output = terminalWriter((data, done) =>
|
||||||
|
t.write(data, () => {
|
||||||
|
probe.render(data)
|
||||||
|
probe.settle()
|
||||||
|
done?.()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
t.attachCustomKeyEventHandler((event) => {
|
t.attachCustomKeyEventHandler((event) => {
|
||||||
const key = event.key.toLowerCase()
|
const key = event.key.toLowerCase()
|
||||||
@@ -441,10 +452,6 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
startResize()
|
startResize()
|
||||||
}
|
}
|
||||||
|
|
||||||
// t.onScroll((ydisp) => {
|
|
||||||
// console.log("Scroll position:", ydisp)
|
|
||||||
// })
|
|
||||||
|
|
||||||
const once = { value: false }
|
const once = { value: false }
|
||||||
let closing = false
|
let closing = false
|
||||||
|
|
||||||
@@ -460,6 +467,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
ws = socket
|
ws = socket
|
||||||
|
|
||||||
const handleOpen = () => {
|
const handleOpen = () => {
|
||||||
|
probe.connect()
|
||||||
local.onConnect?.()
|
local.onConnect?.()
|
||||||
scheduleSize(t.cols, t.rows)
|
scheduleSize(t.cols, t.rows)
|
||||||
}
|
}
|
||||||
@@ -560,6 +568,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
<div
|
<div
|
||||||
ref={container}
|
ref={container}
|
||||||
data-component="terminal"
|
data-component="terminal"
|
||||||
|
{...{ [terminalAttr]: id }}
|
||||||
data-prevent-autofocus
|
data-prevent-autofocus
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
style={{ "background-color": terminalColors().background }}
|
style={{ "background-color": terminalColors().background }}
|
||||||
|
|||||||
64
packages/app/src/testing/terminal.ts
Normal file
64
packages/app/src/testing/terminal.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
export const terminalAttr = "data-pty-id"
|
||||||
|
|
||||||
|
export type TerminalProbeState = {
|
||||||
|
connected: boolean
|
||||||
|
rendered: string
|
||||||
|
settled: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type E2EWindow = Window & {
|
||||||
|
__opencode_e2e?: {
|
||||||
|
terminal?: {
|
||||||
|
enabled?: boolean
|
||||||
|
terminals?: Record<string, TerminalProbeState>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const seed = (): TerminalProbeState => ({
|
||||||
|
connected: false,
|
||||||
|
rendered: "",
|
||||||
|
settled: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const root = () => {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
const state = (window as E2EWindow).__opencode_e2e?.terminal
|
||||||
|
if (!state?.enabled) return
|
||||||
|
state.terminals ??= {}
|
||||||
|
return state.terminals
|
||||||
|
}
|
||||||
|
|
||||||
|
export const terminalProbe = (id: string) => {
|
||||||
|
const set = (next: Partial<TerminalProbeState>) => {
|
||||||
|
const terms = root()
|
||||||
|
if (!terms) return
|
||||||
|
terms[id] = { ...(terms[id] ?? seed()), ...next }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init() {
|
||||||
|
set(seed())
|
||||||
|
},
|
||||||
|
connect() {
|
||||||
|
set({ connected: true })
|
||||||
|
},
|
||||||
|
render(data: string) {
|
||||||
|
const terms = root()
|
||||||
|
if (!terms) return
|
||||||
|
const prev = terms[id] ?? seed()
|
||||||
|
terms[id] = { ...prev, rendered: prev.rendered + data }
|
||||||
|
},
|
||||||
|
settle() {
|
||||||
|
const terms = root()
|
||||||
|
if (!terms) return
|
||||||
|
const prev = terms[id] ?? seed()
|
||||||
|
terms[id] = { ...prev, settled: prev.settled + 1 }
|
||||||
|
},
|
||||||
|
drop() {
|
||||||
|
const terms = root()
|
||||||
|
if (!terms) return
|
||||||
|
delete terms[id]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user