import assert from 'node:assert/strict';
import { existsSync } from 'node:fs';
import { mkdtemp } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { createServer as createPortProbe } from 'node:net';
import { homedir, tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createServer } from 'vite';
import vue from '@vitejs/plugin-vue';
// Mount real SFCs, styles and API functions without loading the business app/session.
const adminRoot = fileURLToPath(new URL('../', import.meta.url));
const localRequire = createRequire(import.meta.url);
const runtimeRequire = createRequire(
path.join(
process.env.CRMEB_BROWSER_TEST_RUNTIME ||
path.join(homedir(), '.cache/codex-runtimes/codex-primary-runtime/dependencies/node'),
'package.json',
),
);
function testDependency(name) {
try {
return localRequire(name);
} catch (error) {
if (error.code !== 'MODULE_NOT_FOUND') throw error;
return runtimeRequire(name);
}
}
const { chromium } = testDependency('playwright');
const { PNG } = testDependency('pngjs');
async function run() {
const artifacts = await mkdtemp(path.join(tmpdir(), 'crmeb-storage-browser-'));
const requests = [];
const issues = [];
const screenshots = [];
const labels = {
settings: '\u8bbe\u7f6e',
save: '\u4fdd\u5b58',
basic: '\u57fa\u7840\u914d\u7f6e',
storage: '\u7a7a\u95f4\u914d\u7f6e',
cancel: '\u53d6\u6d88',
statusTitle: '\u5207\u6362\u72b6\u6001',
configuration: '\u4fee\u6539\u914d\u7f6e\u4fe1\u606f',
configurationTitle: '\u914d\u7f6e\u4fe1\u606f',
addStorage: '\u6dfb\u52a0\u5b58\u50a8\u7a7a\u95f4',
addStorageTitle: '\u6dfb\u52a0\u4e91\u7a7a\u95f4',
return: '\u8fd4\u56de',
};
const commonConfig = {
image_thumb_status: 1,
thumb_big_width: 800,
thumb_big_height: 800,
thumb_mid_width: 300,
thumb_mid_height: 300,
thumb_small_width: 150,
thumb_small_height: 150,
image_watermark_status: 1,
watermark_type: 1,
watermark_opacity: 0,
watermark_rotate: 0,
watermark_position: 1,
watermark_x: 0,
watermark_y: 0,
watermark_image: '',
watermark_text: '',
watermark_text_size: 16,
watermark_text_color: '#000000',
watermark_text_angle: 0,
};
function fixtureDriver(id = 7) {
const builtin = Number(id) === 1;
return {
id: builtin ? 1 : 7,
driver_key: builtin ? 'local' : 'oss',
driver_name: builtin ? 'Fixture Local' : 'Fixture OSS',
sort: builtin ? 0 : 1,
status: builtin ? 1 : 0,
is_builtin: builtin ? 1 : 0,
icon: '',
config_fields: builtin
? []
: [
{
field: 'accessKey',
label: 'AccessKey ID',
type: 'text',
group: 'Keys',
value: '********',
required: true,
},
{
field: 'secretKey',
label: 'AccessKey Secret',
type: 'password',
group: 'Keys',
value: '********',
required: true,
},
],
bucket_fields: [{ field: 'name', label: 'Name', type: 'text', value: '', required: true }],
};
}
const storageRows = [
{
id: 71,
name: 'fixture-bucket',
_region: 'Fixture region',
domain: 'https://cdn.fixture.invalid/assets/',
cname: 'fixture-bucket.oss-cn-fixture.invalid',
status: 0,
_add_time: '2026-01-01',
_update_time: '2026-01-02',
},
];
const virtualIds = {
'virtual:storage-browser-entry': '\0storage-browser-entry',
'virtual:storage-browser-request': '\0storage-browser-request',
'virtual:storage-browser-upload': '\0storage-browser-upload',
'virtual:storage-browser-modal': '\0storage-browser-modal',
'virtual:storage-browser-public': '\0storage-browser-public',
'virtual:storage-browser-util': '\0storage-browser-util',
};
const requestModule = `
export default async function request(config) {
const url = new URL('/__storage_browser_api/' + config.url, location.origin);
for (const [key, value] of Object.entries(config.params || {})) {
if (value !== undefined && value !== null) url.searchParams.set(key, value);
}
const response = await fetch(url, {
method: config.method.toUpperCase(),
credentials: 'omit',
headers: { 'Content-Type': 'application/json' },
body: config.data === undefined ? undefined : JSON.stringify(config.data),
});
const result = await response.json();
if (!response.ok || result.status !== 200) throw result;
return result;
}
`;
const entryModule = `
import { createApp, h } from 'vue';
import { createRouter, createMemoryHistory } from 'vue-router';
import ElementPlus from 'element-plus';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import * as icons from '@element-plus/icons-vue';
import 'element-plus/dist/index.css';
import '@/theme/index.scss';
import '@/styles/index.scss';
import '@/styles/el-icon-compat.css';
import UploadDriverList from '@/pages/driver/uploadDriver/index.vue';
import Pagination from '@/components/Pagination/index.vue';
import dbClick from '@/directive/module/dbClick.js';
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: '/', component: { render: () => null } }],
});
const app = createApp({
render: () => h('main', { class: 'storage-browser-workspace' }, h(UploadDriverList)),
});
app.use(router);
app.use(ElementPlus, { locale: zhCn });
app.component('Pagination', Pagination);
for (const [name, component] of Object.entries(icons)) app.component(name, component);
app.directive('auth', {});
app.directive('db-click', dbClick);
app.config.globalProperties.$copyText = async () => {};
await router.push('/');
await router.isReady();
app.mount('#app');
`;
const html = `
Isolated storage browser test`;
function fixturePlugin() {
return {
name: 'isolated-storage-browser-fixtures',
enforce: 'pre',
resolveId(id) {
return virtualIds[id];
},
load(id) {
if (id === virtualIds['virtual:storage-browser-entry']) return entryModule;
if (id === virtualIds['virtual:storage-browser-request']) return requestModule;
if (id === virtualIds['virtual:storage-browser-upload']) return 'export default { render() { return null; } };';
if (id === virtualIds['virtual:storage-browser-modal'])
return "export default () => { throw Error('Fixture does not permit modalForm'); };";
if (id === virtualIds['virtual:storage-browser-public'])
return "export const modalSure = () => { throw Error('Fixture does not permit deletion'); };";
if (id === virtualIds['virtual:storage-browser-util'])
return "export const getCookies = () => { throw Error('Fixture does not permit business cookie access'); };";
},
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
const url = new URL(req.url, 'http://127.0.0.1');
if (url.pathname === '/') {
res.setHeader('Content-Type', 'text/html');
res.end(await server.transformIndexHtml('/', html));
return;
}
if (!url.pathname.startsWith('/__storage_browser_api/')) return next();
for (const header of ['cookie', 'authorization', 'authori-zation'])
assert.equal(req.headers[header], undefined);
let body = '';
for await (const chunk of req) body += chunk;
const api = url.pathname.slice('/__storage_browser_api/'.length);
const data = body ? JSON.parse(body) : undefined;
requests.push({ api, method: req.method, data, params: Object.fromEntries(url.searchParams) });
let response;
if (req.method === 'GET' && api === 'driver/upload_driver/list') {
response = { list: [fixtureDriver(1), fixtureDriver(7)] };
} else if (req.method === 'GET' && /^driver\/upload_driver\/detail\/(1|7)$/.test(api)) {
response = fixtureDriver(api.split('/').at(-1));
} else if (req.method === 'GET' && /^driver\/upload_driver\/common_config\/(local|oss)$/.test(api)) {
response = { ...commonConfig };
} else if (req.method === 'GET' && api === 'system/config/storage') {
assert.equal(url.searchParams.get('driver_key'), 'oss');
response = { list: storageRows.map((row) => ({ ...row })), count: storageRows.length };
} else if (req.method === 'POST' && api === 'driver/upload_driver/save_config/7') {
await new Promise((resolve) => setTimeout(resolve, 150));
response = [];
} else {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ status: 500, msg: 'Unsupported isolated fixture request', data: [] }));
return;
}
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ status: 200, msg: 'Fixture success', data: response }));
});
},
};
}
async function screenshot(page, name) {
const filename = path.join(artifacts, name + '.png');
const buffer = await page.screenshot({ path: filename, fullPage: false, animations: 'disabled' });
screenshots.push(filename);
const png = PNG.sync.read(buffer);
const colors = new Set();
let ink = 0;
for (let offset = 0; offset < png.data.length; offset += 16) {
const [r, g, b] = png.data.subarray(offset, offset + 3);
colors.add(`${r >> 4},${g >> 4},${b >> 4}`);
if (Math.min(r, g, b) < 210) ink++;
}
assert(colors.size > 12 && ink > 400, `${name}: screenshot must contain rendered UI pixels`);
}
async function checkLayout(page, name, selector) {
const result = await page.locator(selector).evaluate((scope) => {
const problems = [];
const visible = (element) => {
const r = element.getBoundingClientRect();
return r.width > 0 && r.height > 0 && getComputedStyle(element).visibility !== 'hidden';
};
const describe = (element) =>
(element.innerText || element.getAttribute('placeholder') || element.tagName)
.trim()
.replace(/\s+/g, ' ')
.slice(0, 45);
const overlap = (a, b) => {
const x = Math.min(a.right, b.right) - Math.max(a.left, b.left);
const y = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
return x > 2 && y > 2;
};
for (const item of scope.querySelectorAll('.el-form-item')) {
if (!visible(item)) continue;
const label = item.querySelector('.el-form-item__label');
const content = item.querySelector('.el-form-item__content');
if (content && content.getBoundingClientRect().width < 20) {
problems.push(
`form content collapsed: ${describe(label || item)} (${Math.round(
content.getBoundingClientRect().width,
)}px)`,
);
}
if (label && content && overlap(label.getBoundingClientRect(), content.getBoundingClientRect())) {
problems.push(`form label overlaps content: ${describe(label)}`);
}
}
for (const toolbar of scope.querySelectorAll('.el-row > .el-col, .form-footer, .el-message-box__btns')) {
const buttons = [...toolbar.querySelectorAll('button')].filter(visible);
for (let i = 0; i < buttons.length; i++) {
for (let j = i + 1; j < buttons.length; j++) {
if (overlap(buttons[i].getBoundingClientRect(), buttons[j].getBoundingClientRect())) {
problems.push(`buttons overlap: ${describe(buttons[i])} / ${describe(buttons[j])}`);
}
}
}
}
const bounds = scope.getBoundingClientRect();
if (bounds.left < -2 || bounds.right > innerWidth + 2) {
problems.push(
`frame exceeds viewport: ${Math.round(bounds.width)}px, x=${Math.round(bounds.left)}..${Math.round(
bounds.right,
)}, viewport=${innerWidth}px`,
);
}
for (const element of scope.querySelectorAll(
'button, .el-input, .el-select, .el-input-number, .el-switch, .el-pagination, .pagination-container, .thumb-item, .position-container',
)) {
if (!visible(element) || element.closest('.el-table')) continue;
const r = element.getBoundingClientRect();
if (r.left < bounds.left - 2 || r.right > bounds.right + 2) {
problems.push(
`control exceeds content width: ${describe(element)} (${Math.round(r.width)}px, x=${Math.round(
r.left,
)}..${Math.round(r.right)}, content=${Math.round(bounds.left)}..${Math.round(bounds.right)})`,
);
}
}
for (const image of scope.querySelectorAll('img')) {
if (visible(image) && (!image.complete || !image.naturalWidth))
problems.push('visible bitmap asset failed to render');
}
for (const button of scope.querySelectorAll('button.el-button')) {
if (!visible(button)) continue;
const span = button.querySelector('span');
if (!span) continue;
const r = button.getBoundingClientRect();
const text = span.getBoundingClientRect();
if (text.left < r.left - 2 || text.right > r.right + 2 || text.top < r.top - 2 || text.bottom > r.bottom + 2) {
problems.push(`button label exceeds button: ${describe(button)}`);
}
}
return [...new Set(problems)];
});
for (const problem of result) issues.push(`${name}: ${problem}`);
}
let server;
let browser;
let activePage;
async function freeLoopbackPort() {
const probe = createPortProbe();
try {
await new Promise((resolve, reject) => {
probe.once('error', reject);
probe.listen(0, '127.0.0.1', resolve);
});
return probe.address().port;
} finally {
if (probe.listening)
await new Promise((resolve, reject) => probe.close((error) => (error ? reject(error) : resolve())));
}
}
try {
const port = await freeLoopbackPort();
server = await createServer({
configFile: false,
root: adminRoot,
cacheDir: path.join(artifacts, 'vite-cache'),
plugins: [fixturePlugin(), vue()],
optimizeDeps: {
noDiscovery: true,
entries: [],
include: ['vue', 'vue-router', 'element-plus', '@element-plus/icons-vue'],
},
resolve: {
alias: [
{ find: '@/libs/request', replacement: 'virtual:storage-browser-request' },
{ find: '@/libs/util', replacement: 'virtual:storage-browser-util' },
{ find: '@/components/uploadPictures', replacement: 'virtual:storage-browser-upload' },
{ find: '@/utils/modalForm', replacement: 'virtual:storage-browser-modal' },
{ find: '@/utils/public', replacement: 'virtual:storage-browser-public' },
{ find: /^vue$/, replacement: path.join(adminRoot, 'node_modules/vue/dist/vue.esm-bundler.js') },
{ find: '@', replacement: path.join(adminRoot, 'src') },
],
},
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler',
silenceDeprecations: ['legacy-js-api', 'import', 'global-builtin', 'color-functions', 'slash-div'],
},
},
},
server: { host: '127.0.0.1', port, hmr: false, strictPort: true, fs: { allow: [adminRoot] } },
logLevel: 'warn',
});
await server.listen();
const origin = `http://127.0.0.1:${server.httpServer.address().port}`;
const channel =
process.env.CRMEB_BROWSER_TEST_CHANNEL || (existsSync(chromium.executablePath()) ? undefined : 'chrome');
browser = await chromium.launch({ headless: true, channel });
for (const [name, viewport] of Object.entries({
desktop: { width: 1440, height: 900 },
mobile: { width: 390, height: 844 },
narrow: { width: 320, height: 844 },
})) {
const context = await browser.newContext({ viewport });
try {
const page = (activePage = await context.newPage());
page.setDefaultTimeout(10000);
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
page.on('console', (message) => {
if (message.type() === 'error') errors.push(message.text());
});
await context.route('**/*', (route) => {
if (new URL(route.request().url()).origin === origin) return route.continue();
errors.push('Unexpected non-loopback network request');
return route.abort();
});
await context.addInitScript(() => {
Storage.prototype.setItem = () => {
throw new Error('Isolated test must not write business browser storage');
};
Storage.prototype.removeItem = () => {
throw new Error('Isolated test must not clear business browser storage');
};
Storage.prototype.clear = () => {
throw new Error('Isolated test must not clear business browser storage');
};
});
await page.goto(origin, { waitUntil: 'networkidle' });
assert.deepEqual(errors, [], `${name}: isolated browser entry must load`);
const row = page.locator('.el-table__body tr').filter({ hasText: 'Fixture OSS' });
await row.getByRole('button', { name: labels.settings, exact: true }).waitFor();
await screenshot(page, `${name}-list`);
await checkLayout(page, `${name}-list`, '.storage-browser-workspace');
await row.getByRole('button', { name: labels.settings, exact: true }).click();
const drawer = page.locator('.el-drawer');
await drawer.getByRole('tab', { name: labels.basic, exact: true }).waitFor();
const secretInput = drawer.locator('.el-form-item').filter({ hasText: 'AccessKey Secret' }).locator('input');
await secretInput.waitFor({ state: 'attached' });
await page.waitForFunction(() => !document.querySelector('.el-drawer .el-loading-mask'));
await screenshot(page, `${name}-edit`);
await checkLayout(page, `${name}-edit`, '.el-drawer__body');
await secretInput.fill('synthetic-browser-key');
const beforeSave = requests.length;
await drawer.getByRole('button', { name: labels.save, exact: true }).click();
await drawer.waitFor({ state: 'hidden' });
const writes = requests.slice(beforeSave).filter((request) => request.method !== 'GET');
assert.equal(writes.length, 1, `${name}: settings save must be one atomic request`);
assert.equal(writes[0].api, 'driver/upload_driver/save_config/7');
assert.equal(writes[0].data.secretKey, 'synthetic-browser-key');
assert.equal(writes[0].data.accessKey, '********');
assert.equal(writes[0].data.thumb_watermark_config.watermark_opacity, 0);
assert.equal(writes[0].data.thumb_watermark_config.watermark_x, 0);
assert.equal(writes[0].data.thumb_watermark_config.image_thumb_status, 1);
await row.getByRole('button', { name: labels.settings, exact: true }).click();
await drawer.getByRole('tab', { name: labels.storage, exact: true }).click();
await drawer.getByText('fixture-bucket', { exact: true }).waitFor();
await screenshot(page, `${name}-storage`);
await checkLayout(page, `${name}-storage`, '.el-drawer__body');
const storageRow = drawer.locator('.el-table__body tr').filter({ hasText: 'fixture-bucket' });
const storageSwitch = storageRow.getByRole('switch');
assert.equal(await storageSwitch.getAttribute('aria-checked'), 'false');
const beforeStatus = requests.length;
await storageRow.locator('.el-switch').click();
const confirmation = page.locator('.el-message-box');
await confirmation.getByText(labels.statusTitle, { exact: true }).waitFor();
await screenshot(page, `${name}-status-confirmation`);
await checkLayout(page, `${name}-status-confirmation`, '.el-message-box');
await confirmation.getByRole('button', { name: labels.cancel, exact: true }).click();
await confirmation.waitFor({ state: 'hidden' });
assert.equal(await storageSwitch.getAttribute('aria-checked'), 'false');
assert.equal(
requests.slice(beforeStatus).filter((request) => request.method !== 'GET').length,
0,
`${name}: cancelled status confirmation must not write`,
);
for (const [command, title, suffix] of [
[labels.configuration, labels.configurationTitle, 'configuration-dialog'],
[labels.addStorage, labels.addStorageTitle, 'add-dialog'],
]) {
await drawer.getByRole('button', { name: command, exact: true }).click();
const dialog = page.getByRole('dialog', { name: title, exact: true });
await dialog.waitFor();
await screenshot(page, `${name}-${suffix}`);
await checkLayout(page, `${name}-${suffix}`, '.el-dialog:visible');
await dialog.getByRole('button', { name: labels.cancel, exact: true }).click();
await dialog.waitFor({ state: 'hidden' });
}
const beforeLocal = requests.length;
await drawer.getByRole('button', { name: labels.return, exact: true }).click();
await drawer.waitFor({ state: 'hidden' });
const localRow = page.locator('.el-table__body tr').filter({ hasText: 'Fixture Local' });
await localRow.getByRole('button', { name: labels.settings, exact: true }).click();
await drawer.locator('.thumb-fields input').first().waitFor();
assert.equal(
await drawer.getByRole('tab', { name: labels.storage, exact: true }).count(),
0,
`${name}: local driver must not expose cloud spaces`,
);
assert.equal(
await drawer.getByText('Keys', { exact: true }).count(),
0,
`${name}: local driver must not expose cloud keys`,
);
assert.equal(await drawer.locator('.thumb-fields input').count(), 6);
assert(
await drawer.locator('.watermark-config input').first().isVisible(),
`${name}: local watermark controls must be visible`,
);
await screenshot(page, `${name}-local-edit`);
await checkLayout(page, `${name}-local-edit`, '.el-drawer__body');
await drawer.locator('.position-container').scrollIntoViewIfNeeded();
await screenshot(page, `${name}-local-watermark`);
await drawer.getByRole('button', { name: labels.return, exact: true }).click();
await drawer.waitFor({ state: 'hidden' });
assert.equal(
requests.slice(beforeLocal).filter((request) => request.method !== 'GET').length,
0,
`${name}: returning from local settings must not write`,
);
assert.deepEqual(errors, [], `${name}: real components must render without browser errors`);
console.log(
`PASS: ${name} real atomic settings save, storage/status cancellation, dialogs, local settings/watermark, bitmap screenshots`,
);
} catch (error) {
if (activePage && !activePage.isClosed()) await screenshot(activePage, `${name}-failure`).catch(() => {});
throw error;
} finally {
activePage = undefined;
await context.close();
}
}
assert.deepEqual(issues, [], 'Visible controls must fit their content area without overlap');
console.log(`PASS: 3 viewport workflows and render/nonoverlap checks; ${screenshots.length} screenshots`);
} catch (error) {
if (activePage && !activePage.isClosed()) await screenshot(activePage, 'failure').catch(() => {});
console.error(error.message);
for (const issue of issues) console.error(issue);
process.exitCode = 1;
} finally {
try {
await browser?.close();
} finally {
await server?.close();
console.log(`Retained isolated screenshots: ${artifacts}`);
for (const filename of screenshots) console.log(filename);
}
}
}
run().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});