V13-FE-006: consolidate approved UI and contract hardening
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
test.describe('KBX application shell',()=>{
|
||||
test('root opens business home without creating a workspace tab',async({page})=>{await page.goto('/');await expect(page).toHaveURL(/\/home$/);await expect(page.getByRole('heading',{name:'홈'})).toBeVisible();await expect(page.getByRole('navigation',{name:'열린 업무'})).toContainText('열린 업무가 없습니다.')})
|
||||
test('mouse user opens menu search and order screen',async({page})=>{await page.goto('/');await page.getByRole('button',{name:/메뉴 검색/}).click();await page.getByRole('searchbox').fill('주문관리');await page.keyboard.press('Enter');await expect(page).toHaveURL(/\/oms\/orders/)})
|
||||
test('keyboard user opens same menu with Ctrl+K',async({page})=>{await page.goto('/');await page.keyboard.press('Control+k');await page.getByRole('searchbox').fill('ERP-INV-001');await page.keyboard.press('Enter');await expect(page).toHaveURL(/\/erp\/inventory/)})
|
||||
test('workspace keeps multiple business contexts',async({page})=>{await page.goto('/oms/orders');await page.goto('/erp/inventory');await expect(page.getByRole('navigation',{name:'열린 업무'})).toContainText('주문관리');await expect(page.getByRole('navigation',{name:'열린 업무'})).toContainText('재고현황')})
|
||||
test('dirty transaction does not close silently',async({page})=>{await page.goto('/oms/orders/new');await page.getByLabel('수취인').fill('홍길동');await page.getByRole('button',{name:'닫기'}).click();await expect(page.getByText('저장하지 않은 변경사항이 있습니다.')).toBeVisible()})
|
||||
})
|
||||
|
||||
test.describe('KBX v29 shell completion',()=>{
|
||||
test('home promotes business quick-start without duplicating dashboard cards',async({page})=>{await page.goto('/home');await expect(page.getByRole('heading',{name:'바로 시작'})).toBeVisible();await expect(page.getByRole('button',{name:/주문관리/}).first()).toBeVisible()})
|
||||
test('menu search keeps focus inside the modal and exposes listbox semantics',async({page})=>{await page.goto('/home');await page.keyboard.press('Control+k');const search=page.getByRole('combobox');await expect(search).toBeFocused();await expect(page.getByRole('listbox')).toBeVisible();await page.keyboard.press('Shift+Tab');await page.keyboard.press('Tab');await expect(page.getByRole('dialog',{name:/메뉴/})).toBeVisible()})
|
||||
test('screen header exposes help utility from the common host',async({page})=>{await page.goto('/oms/orders');await expect(page.getByRole('button',{name:'도움말'}).first()).toBeVisible()})
|
||||
})
|
||||
|
||||
|
||||
test.describe('KBX v30 shell recovery',()=>{
|
||||
test('unknown deep link provides safe navigation recovery',async({page})=>{await page.goto('/unknown/deep/link');await expect(page.getByRole('heading',{name:'화면을 찾을 수 없습니다.'})).toBeVisible();await page.getByRole('button',{name:'메뉴 검색'}).click();await expect(page.getByRole('dialog')).toBeVisible()})
|
||||
test('global module selector and side navigation share one module context',async({page})=>{await page.goto('/home');const moduleSelect=page.getByLabel('업무 모듈');await moduleSelect.selectOption('ERP');await expect(page.getByRole('heading',{name:'ERP 업무'})).toBeVisible()})
|
||||
})
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { requireKbxScenario, scenarioTitle } from './helpers/kbxScenario'
|
||||
|
||||
const queueRecovery=requireKbxScenario('scenario.common.operations.queue-recovery')
|
||||
|
||||
test.describe('COMMON-OPS-001 업무 예외 센터', () => {
|
||||
test(scenarioTitle(queueRecovery.id), async ({ page }) => {
|
||||
await page.goto('/operations/exceptions')
|
||||
await expect(page.locator('[data-screen-id="COMMON-OPS-001"]')).toBeVisible()
|
||||
await expect(page.locator('[data-kbx-surface="exception-summary"]')).toBeVisible()
|
||||
await expect(page.locator('[data-kbx-surface="queue/content"]')).toBeVisible()
|
||||
await expect(page.getByText(/정상 건은 표시하지 않습니다/)).toBeVisible()
|
||||
})
|
||||
|
||||
test('HTTP 200이어도 해결 거절이면 성공으로 가장하지 않고 authoritative receipt로 복구한다', async ({ page }) => {
|
||||
await page.route('**/api/operations/work-items/resolve', route => route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ resolvedCount: 0, rejectedCount: 1 }),
|
||||
}))
|
||||
|
||||
await page.goto('/operations/exceptions')
|
||||
await page.getByText('주문 재고 부족', { exact: true }).dblclick()
|
||||
await expect(page.getByRole('button', { name: '해결 처리' })).toBeVisible()
|
||||
await page.getByRole('button', { name: '해결 처리' }).click()
|
||||
|
||||
const receipt=page.locator('[data-kbx-surface="authoritative-action-receipt"]')
|
||||
await expect(receipt).toBeVisible()
|
||||
await expect(receipt).toBeFocused()
|
||||
await expect(receipt).toContainText('해결 처리가 완료되지 않았습니다.')
|
||||
await expect(receipt).toContainText('해결 0건')
|
||||
await expect(receipt).toContainText('거절 1건')
|
||||
await expect(page.getByRole('button', { name: '최신 상태 조회' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('다음 긴급건은 demo DTO의 critical work item을 실제 Drawer로 연다', async ({ page }) => {
|
||||
await page.goto('/operations/exceptions')
|
||||
const nextUrgent=page.getByRole('button', { name: '다음 긴급건' })
|
||||
await expect(nextUrgent).toBeEnabled()
|
||||
await nextUrgent.click()
|
||||
await expect(page.getByText('출고 연계 지연', { exact: true })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: '재처리' })).toBeVisible()
|
||||
})
|
||||
})
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { requireKbxScenario, scenarioTitle } from './helpers/kbxScenario'
|
||||
|
||||
const mismatchRecovery=requireKbxScenario('scenario.common.reconcile.mismatch-recovery')
|
||||
|
||||
test.describe('COMMON-REC-001 업무 데이터 대사', () => {
|
||||
test(scenarioTitle(mismatchRecovery.id), async ({ page }) => {
|
||||
await page.goto('/operations/reconcile')
|
||||
await expect(page.locator('[data-screen-id="COMMON-REC-001"]')).toBeVisible()
|
||||
await expect(page.locator('[data-kbx-surface="criteria/search"]')).toBeVisible()
|
||||
await expect(page.locator('[data-kbx-surface="comparison-grid"]')).toBeVisible()
|
||||
await expect(page.getByText('ORD-260811-0001', { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
test('예외등록 HTTP 200의 skippedCount를 실제 업무 결과로 표시하고 원천값은 변경하지 않는다', async ({ page }) => {
|
||||
await page.route('**/api/reconcile/items/create-exceptions', route => route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ createdOrUpdatedCount: 0, skippedCount: 1 }),
|
||||
}))
|
||||
|
||||
await page.goto('/operations/reconcile')
|
||||
await page.getByText('ORD-260811-0001', { exact: true }).dblclick()
|
||||
await expect(page.getByText(/원천값을 화면에서 직접 덮어쓰지 않습니다/)).toBeVisible()
|
||||
await page.getByRole('button', { name: '이 건 예외 등록' }).click()
|
||||
|
||||
const receipt=page.locator('[data-kbx-surface="reconcile-authoritative-receipt"]')
|
||||
await expect(receipt).toBeVisible()
|
||||
await expect(receipt).toBeFocused()
|
||||
await expect(receipt).toContainText('예외 업무 등록이 일부 처리되었습니다.')
|
||||
await expect(receipt).toContainText('생성/갱신 0건')
|
||||
await expect(receipt).toContainText('건너뜀 1건')
|
||||
await expect(page.getByRole('button', { name: '최신 대사 조회' })).toBeVisible()
|
||||
})
|
||||
})
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('T05 inventory keeps master context and exposes locations, history, drill-down', async ({ page }) => {
|
||||
await page.goto('/erp/inventory')
|
||||
await expect(page.getByText('창고/로케이션')).toBeVisible()
|
||||
await expect(page.getByText('재고이력')).toBeVisible()
|
||||
await expect(page.getByText('품목을 선택하면 로케이션과 재고이력을 함께 조회합니다.')).toBeVisible()
|
||||
})
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { requireKbxScenario, scenarioTitle } from './helpers/kbxScenario'
|
||||
|
||||
const masterRecovery=requireKbxScenario('scenario.erp.item-master.keyboard-recovery')
|
||||
|
||||
test.describe('ERP-MST-ITEM-001 T02 Lifecycle',()=>{
|
||||
test(scenarioTitle(masterRecovery.id),async({page})=>{
|
||||
await page.goto('/erp/items')
|
||||
await expect(page.locator('[data-screen-id="ERP-MST-ITEM-001"]')).toBeVisible()
|
||||
await expect(page.locator('[data-kbx-surface="master-list"]')).toBeVisible()
|
||||
await expect(page.locator('[data-kbx-surface="detail"]')).toBeVisible()
|
||||
await expect(page.getByRole('button',{name:/저장 F8/})).toBeDisabled()
|
||||
})
|
||||
})
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('T04 item price fast entry supports lookup, paste, fill down and save', async ({ page }) => {
|
||||
await page.goto('/erp/item-prices')
|
||||
await expect(page.getByText('품목 단가 일괄등록')).toBeVisible()
|
||||
await expect(page.getByText('Excel 붙여넣기')).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: '행 추가' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: '아래 채우기' })).toBeVisible()
|
||||
})
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
test('ERP-PUR-001 supports keyboard-first header/detail purchase entry', async ({page}) => {
|
||||
await page.goto('/erp/purchases/new'); await expect(page.locator('[data-screen-id="ERP-PUR-001"]')).toBeVisible(); await expect(page.getByText('현재 상태')).toBeVisible();
|
||||
})
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { kbxTestScenarioCatalog } from '@kbx/contracts'
|
||||
|
||||
export function requireKbxScenario(id: keyof typeof kbxTestScenarioCatalog) {
|
||||
const scenario = kbxTestScenarioCatalog[id]
|
||||
if (!scenario) throw new Error(`Unknown KBX scenario: ${String(id)}`)
|
||||
return scenario
|
||||
}
|
||||
|
||||
export function scenarioTitle(id: keyof typeof kbxTestScenarioCatalog) {
|
||||
const s = requireKbxScenario(id)
|
||||
return `[${s.id}] ${s.title}`
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { createHash, randomUUID } from 'node:crypto'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
export type KbxScenarioEvidenceArtifact = {
|
||||
type: string
|
||||
path: string
|
||||
sha256?: string
|
||||
}
|
||||
|
||||
export async function sha256File(path: string) {
|
||||
const bytes = await readFile(path)
|
||||
return createHash('sha256').update(bytes).digest('hex')
|
||||
}
|
||||
|
||||
export function createScenarioRunId() {
|
||||
return `kbx-scenario-${randomUUID()}`
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { requireKbxScenario, scenarioTitle } from './helpers/kbxScenario'
|
||||
|
||||
const orderKeyboard = requireKbxScenario('scenario.oms.order-register.keyboard')
|
||||
const shellDirty = requireKbxScenario('scenario.shell.unsaved-tab')
|
||||
|
||||
test(scenarioTitle(orderKeyboard.id), async ({ page }) => {
|
||||
await page.goto('/oms/orders/register')
|
||||
await page.keyboard.press('F2')
|
||||
await expect(page.getByRole('dialog')).toBeVisible()
|
||||
await page.keyboard.press('Escape')
|
||||
await page.keyboard.press('F8')
|
||||
// Host app should seed synthetic lookup data and assert saved/problem outcome.
|
||||
})
|
||||
|
||||
test(scenarioTitle(shellDirty.id), async ({ page }) => {
|
||||
await page.goto('/oms/orders/register')
|
||||
await page.getByLabel('수취인').fill('테스트수취인')
|
||||
await page.getByRole('button', { name: /주문등록.*닫기|닫기/ }).click()
|
||||
await expect(page.getByText('저장하지 않은 변경사항이 있습니다.')).toBeVisible()
|
||||
})
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
for (const scenario of [
|
||||
{ density:'compact', width:1440, height:900 },
|
||||
{ density:'comfortable', width:1440, height:900 },
|
||||
{ density:'touch', width:390, height:844 },
|
||||
] as const) {
|
||||
test(`KBX catalog ${scenario.density} visual baseline`, async ({ page }) => {
|
||||
await page.setViewportSize({ width:scenario.width, height:scenario.height })
|
||||
await page.goto(`/internal/kbx/catalog?density=${scenario.density}`)
|
||||
await expect(page.getByRole('heading',{name:'KBX 컴포넌트 카탈로그'})).toBeVisible()
|
||||
await expect(page).toHaveScreenshot(`kbx-catalog-${scenario.density}.png`, { fullPage:true })
|
||||
})
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
test('experiment governance screen keeps safe scope visible',async({page})=>{await page.goto('/internal/kbx/experiments');await expect(page.getByText('안전 범위')).toBeVisible();await expect(page.getByText(/권한·민감정보·Domain Rule/)).toBeVisible()})
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('KBX Fast Entry / Bulk Grid',()=>{
|
||||
test('catalog exposes explicit fast-entry actions and server-side whole-result selection',async({page})=>{
|
||||
await page.goto('/internal/kbx/catalog?density=compact')
|
||||
await expect(page.getByRole('heading',{name:'Fast Entry Grid'})).toBeVisible()
|
||||
await expect(page.getByRole('button',{name:'행 추가'})).toBeVisible()
|
||||
await expect(page.getByRole('button',{name:'행 복제'})).toBeVisible()
|
||||
await expect(page.getByRole('button',{name:'아래 채우기'})).toBeVisible()
|
||||
await expect(page.getByRole('button',{name:/검색결과 82,415건 전체선택/})).toBeVisible()
|
||||
})
|
||||
})
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('core ERP keyboard contract remains consistent', async ({ page }) => {
|
||||
await page.goto('/oms/orders/new')
|
||||
await page.keyboard.press('F2')
|
||||
await page.keyboard.press('Escape')
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.press('Shift+Tab')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.press('F8')
|
||||
await expect(page.locator('body')).toBeVisible()
|
||||
})
|
||||
|
||||
test('search screen keeps F3 and mouse-complete behavior', async ({ page }) => {
|
||||
await page.goto('/oms/orders')
|
||||
await page.keyboard.press('F3')
|
||||
await expect(page.getByRole('button',{name:/조회/})).toBeVisible()
|
||||
})
|
||||
|
||||
test('menu search is available by mouse and Ctrl+K', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await expect(page.getByRole('button',{name:/메뉴 검색/})).toBeVisible()
|
||||
await page.keyboard.press('Control+K')
|
||||
await page.keyboard.type('재고')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
test('component tabs support arrow, Home and End navigation', async ({ page }) => {
|
||||
await page.goto('/internal/kbx/catalog?density=compact')
|
||||
const basic=page.getByRole('tab',{name:'기본'})
|
||||
const audit=page.getByRole('tab',{name:/변경이력/})
|
||||
await basic.focus()
|
||||
await page.keyboard.press('ArrowRight')
|
||||
await expect(audit).toHaveAttribute('aria-selected','true')
|
||||
await page.keyboard.press('Home')
|
||||
await expect(basic).toHaveAttribute('aria-selected','true')
|
||||
await page.keyboard.press('End')
|
||||
await expect(audit).toHaveAttribute('aria-selected','true')
|
||||
await page.keyboard.press('ArrowLeft')
|
||||
await expect(basic).toHaveAttribute('aria-selected','true')
|
||||
})
|
||||
|
||||
|
||||
test('lookup Escape returns focus to the invoking field instead of losing keyboard context', async ({ page }) => {
|
||||
await page.goto('/oms/orders/new')
|
||||
const customerCode=page.getByRole('textbox',{name:'거래처 코드'})
|
||||
await customerCode.focus()
|
||||
await page.keyboard.press('F2')
|
||||
await expect(page.getByRole('dialog')).toBeVisible()
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(customerCode).toBeFocused()
|
||||
})
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('OMS-CLM-001 클레임 업무 흐름', () => {
|
||||
test('선택 계약과 WorkflowBar가 실제 승인 Command까지 연결된다', async ({ page }) => {
|
||||
await page.goto('/oms/claims')
|
||||
await expect(page.locator('[data-screen-id="OMS-CLM-001"]')).toBeVisible()
|
||||
await page.keyboard.press('F3')
|
||||
await expect(page.getByText('CLM-260811-01', { exact: true })).toBeVisible()
|
||||
|
||||
await page.getByText('CLM-260811-01', { exact: true }).click()
|
||||
const workflow=page.getByLabel('업무 상태')
|
||||
await expect(workflow).toBeVisible()
|
||||
await expect(workflow.getByRole('button', { name: '승인', exact: true })).toBeVisible()
|
||||
await workflow.getByRole('button', { name: '승인', exact: true }).click()
|
||||
|
||||
const receipt=page.locator('[data-kbx-surface="claim-action-receipt"]')
|
||||
await expect(receipt).toBeVisible()
|
||||
await expect(receipt).toBeFocused()
|
||||
await expect(receipt).toContainText('요청 1건 · 성공 1건 · 실패 0건')
|
||||
})
|
||||
|
||||
test('승인 상태는 처리 시작으로 이어지고 보류는 명시적 상태로 표시된다', async ({ page }) => {
|
||||
await page.goto('/oms/claims')
|
||||
await page.getByLabel('상태').selectOption('APPROVED')
|
||||
await page.keyboard.press('F3')
|
||||
await page.getByText('CLM-260811-02', { exact: true }).click()
|
||||
|
||||
const workflow=page.getByLabel('업무 상태')
|
||||
await expect(workflow).toContainText('승인')
|
||||
await workflow.getByRole('button', { name: '처리 시작', exact: true }).click()
|
||||
await expect(page.locator('[data-kbx-surface="claim-action-receipt"]')).toContainText('처리 시작 결과')
|
||||
|
||||
await page.getByLabel('상태').selectOption('HOLD')
|
||||
await page.keyboard.press('F3')
|
||||
await page.getByText('CLM-260811-04', { exact: true }).click()
|
||||
await expect(page.getByLabel('업무 상태')).toContainText('보류')
|
||||
await expect(page.getByLabel('업무 상태')).toContainText('현재 상태에서 실행 가능한 다음 작업이 없습니다.')
|
||||
})
|
||||
})
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('OMS-ORD-003 주문 Excel Import Golden Flow', () => {
|
||||
test('파일 → 매핑 → 검증 → 미리보기 → 반영 → 부분성공 복구', async ({ page }) => {
|
||||
await page.goto('/oms/orders/import')
|
||||
await expect(page.getByRole('heading', { name: '주문 Excel 업로드' })).toBeVisible()
|
||||
|
||||
// Demo API validates the browser workflow, not workbook parsing. Keep this fixture in-memory so
|
||||
// the E2E contract never depends on an untracked binary workbook in the repository.
|
||||
await page.locator('input[type=file]').setInputFiles({
|
||||
name: 'orders-valid-and-invalid.xlsx',
|
||||
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
buffer: Buffer.from('KBX E2E demo workbook'),
|
||||
})
|
||||
|
||||
const mappingHeading=page.getByRole('heading',{name:'필드 매핑'})
|
||||
await expect(mappingHeading).toBeVisible()
|
||||
await expect(mappingHeading).toBeFocused()
|
||||
await page.getByRole('button', { name: '검증 시작' }).click()
|
||||
|
||||
const previewHeading=page.getByRole('heading',{name:'반영 미리보기'})
|
||||
await expect(previewHeading).toBeVisible({ timeout: 30_000 })
|
||||
await expect(previewHeading).toBeFocused()
|
||||
await expect(page.getByText('오류').first()).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: '첫 오류' }).click()
|
||||
await expect(page.locator('[data-import-error-index="0"]')).toBeFocused()
|
||||
|
||||
await page.getByRole('button', { name: '정상 데이터 반영' }).click()
|
||||
await page.getByRole('button', { name: /건 반영$/ }).click()
|
||||
|
||||
const resultHeading=page.getByRole('heading',{name:'반영 결과'})
|
||||
await expect(resultHeading).toBeVisible({ timeout: 30_000 })
|
||||
await expect(resultHeading).toBeFocused()
|
||||
await expect(page.getByText(/정상 데이터는 반영되었고 오류 데이터는 제외되었습니다/)).toBeVisible()
|
||||
await expect(page.getByRole('button',{name:'오류파일 다운로드'})).toBeVisible()
|
||||
})
|
||||
})
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('OMS-ORD-001 주문관리', () => {
|
||||
test('F3 조회 후 주문을 선택할 수 있다', async ({ page }) => {
|
||||
await page.goto('/oms/orders')
|
||||
await page.keyboard.press('F3')
|
||||
|
||||
const screen = page.locator('[data-screen-id="OMS-ORD-001"]')
|
||||
await expect(screen).toBeVisible()
|
||||
|
||||
// 실제 프로젝트에서는 API fixture를 고정하고 grid row selector를 KBX testing helper로 감싼다.
|
||||
})
|
||||
|
||||
test('선택 전 출고지시는 비활성화된다', async ({ page }) => {
|
||||
await page.goto('/oms/orders')
|
||||
await expect(page.getByRole('button', { name: /출고지시/ })).toBeDisabled()
|
||||
})
|
||||
})
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('OMS-ORD-002 주문등록 Golden Screen', () => {
|
||||
test('키보드 중심 주문등록: F2 lookup → detail entry → F8 save', async ({ page }) => {
|
||||
await page.goto('/oms/orders/new')
|
||||
|
||||
await expect(page.locator('[data-screen-id="OMS-ORD-002"]')).toBeVisible()
|
||||
|
||||
// 거래처 Lookup: real test environment should seed deterministic customer fixture.
|
||||
const customerCode = page.getByLabel('코드').first()
|
||||
await customerCode.focus()
|
||||
await page.keyboard.press('F2')
|
||||
await expect(page.getByText('거래처 검색')).toBeVisible()
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
// Header validation is visible without losing entered data.
|
||||
await page.keyboard.press('F8')
|
||||
await expect(page.getByText(/항목을 확인하세요/)).toBeVisible()
|
||||
})
|
||||
})
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('KBX production resilience UX', () => {
|
||||
test('version conflict does not silently overwrite latest server data', async ({ page }) => {
|
||||
await page.goto('/oms/orders/new')
|
||||
// Host repository should stub a 409 KbxConflictProblem after editing.
|
||||
// Verify KbxConflictResolver shows latest server values and no second mutation occurs automatically.
|
||||
})
|
||||
|
||||
test('long-running operation remains visible after route change', async ({ page }) => {
|
||||
await page.goto('/oms/orders/import')
|
||||
// Start a background import, navigate away, then verify the runtime Operation Center still exposes progress.
|
||||
})
|
||||
|
||||
test('degraded mode is visible without blocking unaffected reads', async ({ page }) => {
|
||||
await page.goto('/oms/orders')
|
||||
// Stub /api/kbx/runtime/notice as degraded and verify the banner is visible while the list remains usable.
|
||||
await expect(page.locator('[data-screen-id="OMS-ORD-001"]')).toBeVisible()
|
||||
})
|
||||
})
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
const taskId = '00000000-0000-0000-0000-000000000101'
|
||||
|
||||
function task(stage: string, version: number, pickedQty = 0) {
|
||||
const completed = stage === 'completed'
|
||||
return {
|
||||
taskId,
|
||||
taskNo: 'PICK-20260808-001',
|
||||
stage,
|
||||
status: completed ? 'COMPLETED' : stage === 'ready' ? 'READY' : 'IN_PROGRESS',
|
||||
completedLines: completed ? 1 : 0,
|
||||
totalLines: 1,
|
||||
completedQty: completed ? 2 : pickedQty,
|
||||
totalQty: 2,
|
||||
version,
|
||||
currentLine: completed ? null : {
|
||||
lineId: '00000000-0000-0000-0000-000000000201',
|
||||
lineNo: 1,
|
||||
locationCode: 'A-03-02',
|
||||
itemId: '00000000-0000-0000-0000-000000000301',
|
||||
itemCode: 'ABC001',
|
||||
itemName: '운동화',
|
||||
itemOption: 'BLACK / 270',
|
||||
barcode: '8801234567890',
|
||||
requiredQty: 2,
|
||||
pickedQty,
|
||||
remainingQty: 2 - pickedQty,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('WMS-PICK-001 피킹 Golden Screen', () => {
|
||||
test('위치 → 상품 스캔을 확인 modal 없이 연속 처리한다', async ({ page }) => {
|
||||
let current = task('ready', 1)
|
||||
|
||||
await page.route(`**/api/wms/picking/tasks/${taskId}`, route =>
|
||||
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(current) }))
|
||||
|
||||
await page.route(`**/api/wms/picking/tasks/${taskId}/start`, route => {
|
||||
current = task('await-location', 2)
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(current) })
|
||||
})
|
||||
|
||||
await page.route(`**/api/wms/picking/tasks/${taskId}/scan`, async route => {
|
||||
const body = route.request().postDataJSON()
|
||||
if (body.barcode === 'LOC-A-03-02') {
|
||||
current = task('await-item', 3)
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({
|
||||
accepted: true, duplicate: false, task: current, feedback: 'success',
|
||||
message: '위치를 확인했습니다. 상품을 스캔하세요.',
|
||||
}) })
|
||||
}
|
||||
|
||||
current = task('await-item', 4, 1)
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({
|
||||
accepted: true, duplicate: false, task: current, feedback: 'success', message: '1개 피킹했습니다.',
|
||||
}) })
|
||||
})
|
||||
|
||||
await page.goto(`/wms/picking/${taskId}`)
|
||||
await expect(page.locator('[data-screen-id="WMS-PICK-001"]')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: '작업 시작' }).click()
|
||||
await expect(page.getByText('위치를 스캔하세요.')).toBeVisible()
|
||||
|
||||
// Keyboard-wedge scanners typically emit fast characters followed by Enter.
|
||||
await page.keyboard.type('LOC-A-03-02', { delay: 10 })
|
||||
await page.keyboard.press('Enter')
|
||||
await expect(page.getByText('상품을 스캔하세요.')).toBeVisible()
|
||||
|
||||
await page.keyboard.type('8801234567890', { delay: 10 })
|
||||
await page.keyboard.press('Enter')
|
||||
await expect(page.getByText('1개 피킹했습니다.')).toBeVisible()
|
||||
await expect(page.getByText('남음').locator('..').getByText('1')).toBeVisible()
|
||||
})
|
||||
|
||||
test('잘못된 위치는 retry queue 대상이 아닌 업무 오류로 즉시 안내한다', async ({ page }) => {
|
||||
let current = task('await-location', 2)
|
||||
|
||||
await page.route(`**/api/wms/picking/tasks/${taskId}`, route =>
|
||||
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(current) }))
|
||||
|
||||
await page.route(`**/api/wms/picking/tasks/${taskId}/scan`, route =>
|
||||
route.fulfill({ status: 422, contentType: 'application/json', body: JSON.stringify({
|
||||
accepted: false, duplicate: false, task: current, feedback: 'error',
|
||||
message: '잘못된 위치입니다. A-03-02 위치로 이동하세요.',
|
||||
}) }))
|
||||
|
||||
await page.goto(`/wms/picking/${taskId}`)
|
||||
await page.keyboard.type('WRONG-LOC', { delay: 10 })
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await expect(page.getByText('잘못된 위치입니다. A-03-02 위치로 이동하세요.')).toBeVisible()
|
||||
await expect(page.getByText('전송 대기')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('오프라인이면 다음 authoritative scan을 받지 않는다', async ({ page, context }) => {
|
||||
await page.route(`**/api/wms/picking/tasks/${taskId}`, route =>
|
||||
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(task('await-item', 3)) }))
|
||||
|
||||
await page.goto(`/wms/picking/${taskId}`)
|
||||
await context.setOffline(true)
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('offline')))
|
||||
|
||||
await expect(page.getByText(/오프라인/)).toBeVisible()
|
||||
await expect(page.getByText('SCAN PAUSED')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('WMS-PICK-001 authoritative retry evidence', () => {
|
||||
test('response loss 재전송은 최초 command의 동일 idempotencyKey를 사용하고 서버 확인 뒤에만 최근 확인을 갱신한다', async ({ page, context }) => {
|
||||
let current = task('await-item', 3)
|
||||
const scanBodies: Array<Record<string, unknown>> = []
|
||||
|
||||
await page.route(`**/api/wms/picking/tasks/${taskId}`, route =>
|
||||
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(current) }))
|
||||
|
||||
await page.route(`**/api/wms/picking/tasks/${taskId}/scan`, async route => {
|
||||
const body=route.request().postDataJSON() as Record<string, unknown>
|
||||
scanBodies.push(body)
|
||||
if(scanBodies.length===1) return route.abort('failed')
|
||||
current=task('await-item',4,1)
|
||||
return route.fulfill({
|
||||
status:200,
|
||||
contentType:'application/json',
|
||||
body:JSON.stringify({accepted:true,duplicate:true,task:current,feedback:'success',message:'재전송된 스캔을 서버에서 확인했습니다.'}),
|
||||
})
|
||||
})
|
||||
|
||||
await page.goto(`/wms/picking/${taskId}`)
|
||||
await page.keyboard.type('8801234567890', { delay: 10 })
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await expect(page.getByText(/서버 확인이 필요합니다/)).toBeVisible()
|
||||
await expect(page.getByText('아직 확인된 스캔 없음')).toBeVisible()
|
||||
|
||||
await context.setOffline(true)
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('offline')))
|
||||
await context.setOffline(false)
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('online')))
|
||||
|
||||
await expect.poll(() => scanBodies.length).toBe(2)
|
||||
expect(scanBodies[0]?.idempotencyKey).toBeTruthy()
|
||||
expect(scanBodies[1]?.idempotencyKey).toBe(scanBodies[0]?.idempotencyKey)
|
||||
await expect(page.getByText(/재전송된 스캔을 서버에서 확인했습니다/)).toBeVisible()
|
||||
await expect(page.getByText(/8801234567890/)).toBeVisible()
|
||||
})
|
||||
})
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
for (const [path,id] of [['/wms/receiving/task-1','WMS-REC-001'],['/wms/putaway/task-1','WMS-PUT-001'],['/wms/counting/task-1','WMS-COUNT-001']] as const) {
|
||||
test(`${id} uses the WMS mobile shell`, async ({page}) => { await page.goto(path); await expect(page.locator(`[data-screen-id="${id}"]`)).toBeVisible() })
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('WMS-WORK-001 운영 Queue', () => {
|
||||
test('첫 진입부터 전체 내 작업과 예외 신호를 숨기지 않는다', async ({ page }) => {
|
||||
await page.goto('/wms/work')
|
||||
await expect(page.locator('[data-screen-id="WMS-WORK-001"]')).toBeVisible()
|
||||
await expect(page.getByText('PICK-260811-009', { exact: true })).toBeVisible()
|
||||
const exceptionSummary=page.getByLabel('업무 예외 요약')
|
||||
await expect(exceptionSummary).toContainText('확인 필요한 작업')
|
||||
await expect(exceptionSummary).toContainText('1')
|
||||
})
|
||||
|
||||
test('예외 필터는 BLOCKED만 좁혀 보고 작업 시작은 fail-closed 한다', async ({ page }) => {
|
||||
await page.goto('/wms/work')
|
||||
await page.getByLabel('업무 예외 요약').getByRole('button').click()
|
||||
await expect(page.getByText('PICK-260811-009', { exact: true })).toBeVisible()
|
||||
await page.getByText('PICK-260811-009', { exact: true }).click()
|
||||
await expect(page.getByRole('button', { name: '선택 작업 시작' })).toBeDisabled()
|
||||
await expect(page.getByText('예외 작업은 행을 열어 원인을 먼저 확인하세요.')).toBeVisible()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user