161 lines
8.7 KiB
JavaScript
161 lines
8.7 KiB
JavaScript
const assert = require('node:assert/strict')
|
|||
|
|
const fs = require('node:fs')
|
||
|
|
const path = require('node:path')
|
||
|
|
const { pathToFileURL } = require('node:url')
|
||
|
|
const { chromium } = require('/Users/macclaw/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright')
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
const directory = '/Volumes/Extreme SSD/yawan-storage/weknora/acceptance'
|
||
|
|
const root = path.resolve(__dirname, '../frontend')
|
||
|
|
const apiOrigin = process.env.VERIFY_API_ORIGIN || 'http://127.0.0.1:18080'
|
||
|
|
const checks = [], requestedAssets = new Set(), unexpectedMutations = [], pageErrors = []
|
||
|
|
const reportPath = path.join(directory, 'lazy-platform-modals-20260919.json')
|
||
|
|
const credentials = JSON.parse(fs.readFileSync(path.join(directory, 'super-admin-login.json'), 'utf8'))
|
||
|
|
const login = await fetch(`${apiOrigin}/api/v1/auth/login`, {
|
||
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ email: credentials.email, password: credentials.password }),
|
||
|
|
})
|
||
|
|
assert.equal(login.status, 200, 'Read-only verification login')
|
||
|
|
const auth = await login.json()
|
||
|
|
process.env.VITE_DEV_PROXY_TARGET = apiOrigin
|
||
|
|
const { preview } = await import(pathToFileURL(path.join(root, 'node_modules/vite/dist/node/index.js')).href)
|
||
|
|
const server = await preview({ root, configFile: path.join(root, 'vite.config.ts'), preview: { host: '127.0.0.1', port: 15174, strictPort: true } })
|
||
|
|
const origin = server.resolvedUrls.local[0].replace(/\/$/, '')
|
||
|
|
let browser
|
||
|
|
async function check(name, fn) {
|
||
|
|
const started = Date.now()
|
||
|
|
try {
|
||
|
|
await fn()
|
||
|
|
checks.push({ name, passed: true, duration_ms: Date.now() - started })
|
||
|
|
console.log(JSON.stringify(checks.at(-1)))
|
||
|
|
} catch (error) {
|
||
|
|
checks.push({ name, passed: false, duration_ms: Date.now() - started, error: error.name, location: error.stack?.split('\n').filter(line => line.trim().startsWith('at ')).slice(0, 3) })
|
||
|
|
console.log(JSON.stringify(checks.at(-1)))
|
||
|
|
throw error
|
||
|
|
}
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
browser = await chromium.launch({ headless: true, channel: 'chrome' })
|
||
|
|
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } })
|
||
|
|
await context.addInitScript(a => {
|
||
|
|
const values = {
|
||
|
|
weknora_user: JSON.stringify({ ...a.user, tenant_id: String(a.user.tenant_id) }),
|
||
|
|
weknora_tenant: JSON.stringify({ ...a.active_tenant, id: '10059' }),
|
||
|
|
weknora_token: a.token, weknora_refresh_token: a.refresh_token,
|
||
|
|
weknora_memberships: JSON.stringify(a.memberships),
|
||
|
|
}
|
||
|
|
for (const [key, value] of Object.entries(values)) localStorage.setItem(key, value)
|
||
|
|
for (const key of ['weknora:new-user-guide-done:v1', 'weknora:contextual-guide-kb-list:v2', 'weknora:contextual-guide-kb-create:v3', 'weknora:contextual-guide-tenant-models:v1']) localStorage.setItem(key, '1')
|
||
|
|
}, auth)
|
||
|
|
await context.route('**/api/**', async route => {
|
||
|
|
const request = route.request()
|
||
|
|
if (!['GET', 'HEAD', 'OPTIONS'].includes(request.method())) {
|
||
|
|
unexpectedMutations.push({ method: request.method(), path: new URL(request.url()).pathname })
|
||
|
|
return route.abort('blockedbyclient')
|
||
|
|
}
|
||
|
|
await route.continue()
|
||
|
|
})
|
||
|
|
const page = await context.newPage()
|
||
|
|
page.setDefaultTimeout(20000)
|
||
|
|
page.on('pageerror', error => pageErrors.push({ name: error.name }))
|
||
|
|
page.on('request', request => {
|
||
|
|
const pathname = new URL(request.url()).pathname
|
||
|
|
if (pathname.startsWith('/assets/')) requestedAssets.add(pathname)
|
||
|
|
})
|
||
|
|
const editor = page.locator('.settings-overlay').filter({ has: page.locator('[data-guide="kb-editor-sidebar"]') })
|
||
|
|
const settings = page.locator('.settings-overlay').filter({ has: page.locator('[data-guide="settings-models"]') })
|
||
|
|
const nameInput = editor.locator('[data-guide="kb-create-name"] input')
|
||
|
|
const create = async () => {
|
||
|
|
await page.locator('[data-guide="kb-list-create"]').first().click()
|
||
|
|
await nameInput.waitFor({ state: 'visible' })
|
||
|
|
}
|
||
|
|
const closeEditor = async () => {
|
||
|
|
await editor.locator('.close-btn').click()
|
||
|
|
await editor.waitFor({ state: 'hidden' })
|
||
|
|
}
|
||
|
|
await check('cold_knowledge_list_defers_hidden_modal_chunks', async () => {
|
||
|
|
await page.goto(`${origin}/platform/knowledge-bases`)
|
||
|
|
await page.locator('[data-guide="kb-list-create"]').first().waitFor({ state: 'visible' })
|
||
|
|
await page.waitForLoadState('networkidle')
|
||
|
|
assert.equal([...requestedAssets].filter(asset => /\/(Settings|KnowledgeBaseEditorModal)-.*\.js$/.test(asset)).length, 0)
|
||
|
|
})
|
||
|
|
await check('command_palette_keyboard_shortcut_remains_registered', async () => {
|
||
|
|
await page.keyboard.press('Meta+k')
|
||
|
|
await page.locator('.cmdk').waitFor({ state: 'visible' })
|
||
|
|
await page.keyboard.press('Escape')
|
||
|
|
await page.locator('.cmdk').waitFor({ state: 'hidden' })
|
||
|
|
})
|
||
|
|
await check('first_create_initializes_and_only_loads_editor', async () => {
|
||
|
|
await create()
|
||
|
|
assert.equal(await nameInput.inputValue(), '')
|
||
|
|
assert.ok([...requestedAssets].some(asset => /\/KnowledgeBaseEditorModal-.*\.js$/.test(asset)))
|
||
|
|
assert.equal([...requestedAssets].filter(asset => /\/Settings-.*\.js$/.test(asset)).length, 0)
|
||
|
|
})
|
||
|
|
await check('create_close_reopen_discards_unsaved_form', async () => {
|
||
|
|
await nameInput.fill('UNSAVED_BROWSER_CHECK')
|
||
|
|
await closeEditor()
|
||
|
|
await create()
|
||
|
|
assert.equal(await nameInput.inputValue(), '')
|
||
|
|
})
|
||
|
|
async function openModelsSettings() {
|
||
|
|
await editor.locator('[data-guide="kb-editor-nav-models"]').click()
|
||
|
|
await editor.locator('[data-guide="kb-create-llm"] .model-selector').click()
|
||
|
|
await page.locator('.add-model-option:visible').click()
|
||
|
|
await settings.waitFor({ state: 'visible' })
|
||
|
|
}
|
||
|
|
await check('lazy_settings_opens_models_from_editor', async () => {
|
||
|
|
await openModelsSettings()
|
||
|
|
assert.ok([...requestedAssets].some(asset => /\/Settings-.*\.js$/.test(asset)))
|
||
|
|
assert.equal(await page.locator('.settings-overlay').count(), 2)
|
||
|
|
})
|
||
|
|
await check('settings_escape_closes_and_reopens_without_losing_editor', async () => {
|
||
|
|
await page.keyboard.press('Escape')
|
||
|
|
await settings.waitFor({ state: 'hidden' })
|
||
|
|
assert.ok(await editor.isVisible())
|
||
|
|
await openModelsSettings()
|
||
|
|
await settings.locator('.close-btn').click()
|
||
|
|
await settings.waitFor({ state: 'hidden' })
|
||
|
|
await closeEditor()
|
||
|
|
})
|
||
|
|
await check('existing_knowledge_editor_loads_and_reopens_correct_resource', async () => {
|
||
|
|
const card = page.locator('.kb-card').filter({ has: page.locator('.more-wrap') }).first()
|
||
|
|
await card.waitFor({ state: 'visible' })
|
||
|
|
const expectedName = await card.locator('.card-title').getAttribute('title')
|
||
|
|
const edit = async () => {
|
||
|
|
const settingsItem = page.locator('.card-more-popup:visible .popup-menu-item').filter({ has: page.locator('.t-icon-setting') })
|
||
|
|
if (!await settingsItem.isVisible()) await card.locator('.more-wrap').click()
|
||
|
|
await settingsItem.click()
|
||
|
|
await nameInput.waitFor({ state: 'visible' })
|
||
|
|
const actualName = await nameInput.inputValue()
|
||
|
|
if (actualName !== expectedName) {
|
||
|
|
console.log(JSON.stringify({ diagnostic: 'editor_name_mismatch', expected_length: expectedName?.length, actual_length: actualName.length, blank: !actualName, normalized_match: actualName.trim() === expectedName?.trim() }))
|
||
|
|
}
|
||
|
|
assert.equal(actualName, expectedName)
|
||
|
|
}
|
||
|
|
await edit()
|
||
|
|
await nameInput.fill('UNSAVED_BROWSER_CHECK')
|
||
|
|
await closeEditor()
|
||
|
|
await edit()
|
||
|
|
await closeEditor()
|
||
|
|
})
|
||
|
|
await check('direct_settings_models_route_has_one_initialized_overlay', async () => {
|
||
|
|
await page.goto(`${origin}/platform/settings?section=models`)
|
||
|
|
await settings.waitFor({ state: 'visible' })
|
||
|
|
assert.equal(await page.locator('.settings-overlay').count(), 1)
|
||
|
|
await page.waitForLoadState('networkidle')
|
||
|
|
})
|
||
|
|
await check('no_browser_errors_or_business_mutations', async () => {
|
||
|
|
assert.equal(pageErrors.length, 0)
|
||
|
|
assert.equal(unexpectedMutations.length, 0)
|
||
|
|
})
|
||
|
|
} finally {
|
||
|
|
if (browser) await browser.close()
|
||
|
|
await new Promise((resolve, reject) => server.httpServer.close(error => error ? reject(error) : resolve()))
|
||
|
|
const report = { checked_at: new Date().toISOString(), origin, api_origin: apiOrigin, checks, passed: checks.length === 9 && checks.every(check => check.passed), requested_assets: [...requestedAssets], page_errors: pageErrors, unexpected_mutations: unexpectedMutations }
|
||
|
|
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2))
|
||
|
|
console.log(JSON.stringify({ report: reportPath, passed: report.passed, checks: checks.length }))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch(error => { console.error(error.name); process.exitCode = 1 })
|