- 매뉴얼: 화면(소메뉴)별 용어·버튼·필드 설명으로 확장 + 기본정보 페이지 신규, 개요에 용어 사전 추가 (종량제 지식 없는 사용자 대상) - "이 화면 설명" 버튼: 화면 경로→매뉴얼 매핑(Config\Manual::screenHelp, manual_help_url_for_path). 워크스페이스 탭은 새 탭으로, 직접 페이지는 새 창으로 - 워크스페이스: 개별 탭 새로고침(↻) 버튼, 탭 2개 이상일 때만 새로고침 경고, 사이드바 하단 링크(매뉴얼 등)도 탭으로 열기 - 임베드: 탭 내 링크/폼 embed 유지(중첩 헤더 방지), 매뉴얼 리다이렉트 embed 유지 - 사이드바 하단: 종합그래프 → 사용자 매뉴얼 링크 - 최근 방문 메뉴: embed 페이지에도 방문 기록, 대시보드는 storage 이벤트로 실시간 갱신 - E2E qa_sweep 추가(주요 화면 콘솔/오버레이/매뉴얼/도움말 매핑 점검) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
4.1 KiB
JavaScript
96 lines
4.1 KiB
JavaScript
const { test, expect } = require('@playwright/test');
|
|
const { login } = require('./helpers/auth');
|
|
|
|
/**
|
|
* 전체 점검 스윕 — 주요 화면 콘솔에러·차단 오버레이 점검 + 매뉴얼/도움말/워크스페이스 통합 검증.
|
|
*/
|
|
async function selectDaegu(page) {
|
|
await page.goto('/admin/select-local-government');
|
|
await page.evaluate(() => {
|
|
const r = document.querySelector('input[name="lg_idx"][value="1"]');
|
|
if (r) { r.checked = true; r.form.submit(); }
|
|
});
|
|
await page.waitForTimeout(700);
|
|
}
|
|
|
|
// kakao 외부 SDK 관련(도메인 미등록 환경) 잡음은 제외
|
|
function appError(msg) {
|
|
return !/kakao|dapi\.kakao|sdk\.js|OPEN_MAP_AND_LOCAL|appkey/i.test(msg);
|
|
}
|
|
|
|
const PAGES = [
|
|
'/', '/bag/inventory', '/bag/order/create', '/bag/bag-orders',
|
|
'/bag/receiving/scanner', '/bag/receiving/batch', '/bag/sale/designated',
|
|
'/bag/issue/create', '/bag/issue', '/bag/flow', '/bag/sales',
|
|
'/bag/reports/daily-summary', '/bag/reports/lot-flow', '/bag/reports/returns',
|
|
'/bag/bag-prices', '/bag/packaging-units', '/bag/code-kinds',
|
|
'/bag/designated-shops', '/bag/designated-shops/browse', '/bag/number-lookup',
|
|
'/bag/manual', '/admin', '/admin/menus', '/admin/users', '/admin/access/login-history',
|
|
];
|
|
|
|
test.describe('QA 스윕', () => {
|
|
test('주요 화면 콘솔 에러·차단 오버레이 점검', async ({ page }) => {
|
|
await login(page, 'admin');
|
|
await selectDaegu(page);
|
|
const problems = [];
|
|
for (const url of PAGES) {
|
|
const errs = [];
|
|
page.removeAllListeners('pageerror');
|
|
page.on('pageerror', (e) => { if (appError(String(e))) errs.push(String(e)); });
|
|
const res = await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(700);
|
|
const status = res ? res.status() : 0;
|
|
const cover = await page.evaluate(() => {
|
|
const cx = Math.floor(innerWidth / 2), cy = Math.floor(innerHeight / 2);
|
|
let n = 0;
|
|
document.querySelectorAll('*').forEach((el) => {
|
|
const s = getComputedStyle(el);
|
|
if ((s.position === 'fixed' || s.position === 'absolute') && s.display !== 'none' &&
|
|
s.visibility !== 'hidden' && parseFloat(s.opacity || '1') > 0.1 && s.pointerEvents !== 'none') {
|
|
const r = el.getBoundingClientRect();
|
|
if (r.width >= innerWidth * 0.85 && r.height >= innerHeight * 0.7 &&
|
|
!/portal-header|sidebar|ws-/.test(el.className || '')) n++;
|
|
}
|
|
});
|
|
return n;
|
|
});
|
|
if (status >= 400) problems.push(`${url} → HTTP ${status}`);
|
|
if (errs.length) problems.push(`${url} → JS오류: ${errs[0]}`);
|
|
if (cover > 0) problems.push(`${url} → 화면 덮는 오버레이 ${cover}개`);
|
|
}
|
|
console.log('>>> SWEEP problems=' + JSON.stringify(problems, null, 0));
|
|
expect(problems, problems.join('\n')).toEqual([]);
|
|
});
|
|
|
|
test('매뉴얼 전체 페이지 렌더', async ({ page }) => {
|
|
await login(page, 'user');
|
|
const slugs = ['overview', 'flow', 'order', 'inventory', 'sales', 'reports', 'basic', 'codes', 'faq'];
|
|
for (const s of slugs) {
|
|
const res = await page.goto('/bag/manual/' + s, { waitUntil: 'domcontentloaded' });
|
|
expect(res.status(), s).toBe(200);
|
|
await expect(page.locator('.manual-prose')).not.toBeEmpty();
|
|
}
|
|
// 미등록 slug → 404
|
|
const bad = await page.goto('/bag/manual/zzz-none', { waitUntil: 'domcontentloaded' });
|
|
expect(bad.status()).toBe(404);
|
|
});
|
|
|
|
test('이 화면 설명 매핑 정확', async ({ page }) => {
|
|
await login(page, 'admin');
|
|
await selectDaegu(page);
|
|
const cases = [
|
|
['/bag/inventory', 'inventory'],
|
|
['/bag/order/create', 'order'],
|
|
['/bag/sale/designated', 'sales'],
|
|
['/bag/flow', 'reports'],
|
|
['/bag/bag-prices', 'basic'],
|
|
['/bag/number-lookup', 'codes'],
|
|
];
|
|
for (const [url, slug] of cases) {
|
|
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
const href = await page.locator('a.no-print', { hasText: '이 화면 설명' }).first().getAttribute('href');
|
|
expect(href, url).toContain('/bag/manual/' + slug);
|
|
}
|
|
});
|
|
});
|