MAI CALL - step 13
pnpm test:e2e verde. Passo 13 completo.
O que foi feito:
Polish:
SyncProvider — dead-letter toast fixo na base do ecrã (cor destructive, botão ✕ para fechar), dispara quando broadcast({ type: 'dead-letter' }) chega via BroadcastChannel
Loading states e empty states já estavam implementados nos passos anteriores
E2E test — e2e/tests/mai-call.spec.ts:
Substitui o ping.spec.ts obsoleto
Arranca ambos os servidores (operator-pwa :3000 + admin-web :3001) com AUTH_DEV_AUTOLOGIN=true
Fluxo completo em 13.7s: formulário → IndexedDB → sync automático → admin queue → claim (OPEN→CLAIMED) → enable RESOLVED filter → resolve dialog → confirm (CLAIMED→RESOLVED)
pnpm test:e2e passa ✓
This commit is contained in:
parent
b7e3208eb2
commit
9418b360bc
@ -38,7 +38,11 @@
|
||||
"Bash(Get-ChildItem -Path \"c:\\\\Users\\\\prdcg\\\\Documents\\\\Git\\\\FieldOps\" -Directory)",
|
||||
"Bash(Select-Object Name)",
|
||||
"Bash(pnpm --filter @repo/admin-web typecheck)",
|
||||
"Bash(pnpm --filter @repo/admin-web build)"
|
||||
"Bash(pnpm --filter @repo/admin-web build)",
|
||||
"Bash(pnpm --filter @repo/e2e exec tsc --noEmit)",
|
||||
"Bash(pnpm exec *)",
|
||||
"Bash(pnpm test:e2e)",
|
||||
"Bash(pnpm -w run test:e2e)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ function RequestCard({
|
||||
claiming: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-xl border border-border bg-card p-4 shadow-sm">
|
||||
<div data-testid="request-card" className="flex flex-col gap-3 rounded-xl border border-border bg-card p-4 shadow-sm">
|
||||
{/* Top row: thumbnail + main info */}
|
||||
<div className="flex gap-3">
|
||||
<Thumbnail photoKey={item.photoKey} />
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { subscribeBroadcast } from '@/lib/queue/broadcast';
|
||||
import { subscribeBroadcast, type SyncMessage } from '@/lib/queue/broadcast';
|
||||
import { runSync } from '@/lib/queue/sync';
|
||||
import { db } from '@/lib/queue/db';
|
||||
|
||||
@ -23,6 +23,7 @@ export const useSyncState = () => useContext(SyncCtx);
|
||||
|
||||
export function SyncProvider({ children }: { children: ReactNode }) {
|
||||
const [state, setState] = useState<SyncState>({ pendingCount: 0, deadLetterCount: 0 });
|
||||
const [failedIds, setFailedIds] = useState<string[]>([]);
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const refreshCounts = useCallback(async () => {
|
||||
@ -38,7 +39,12 @@ export function SyncProvider({ children }: { children: ReactNode }) {
|
||||
useEffect(() => {
|
||||
refreshCounts();
|
||||
|
||||
const unsub = subscribeBroadcast(async () => refreshCounts());
|
||||
const unsub = subscribeBroadcast(async (msg: SyncMessage) => {
|
||||
await refreshCounts();
|
||||
if (msg.type === 'dead-letter') {
|
||||
setFailedIds((prev) => [...prev, msg.clientRequestId]);
|
||||
}
|
||||
});
|
||||
|
||||
const onOnline = () => sync();
|
||||
const onVisible = () => {
|
||||
@ -48,12 +54,10 @@ export function SyncProvider({ children }: { children: ReactNode }) {
|
||||
window.addEventListener('online', onOnline);
|
||||
document.addEventListener('visibilitychange', onVisible);
|
||||
|
||||
// Poll every 10s as a fallback
|
||||
timerRef.current = setInterval(() => {
|
||||
if (navigator.onLine) sync();
|
||||
}, 10_000);
|
||||
|
||||
// Register Background Sync if SW + API available
|
||||
if ('serviceWorker' in navigator && 'SyncManager' in window) {
|
||||
navigator.serviceWorker.ready
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -61,7 +65,6 @@ export function SyncProvider({ children }: { children: ReactNode }) {
|
||||
.catch(() => {/* not supported or permission denied */});
|
||||
}
|
||||
|
||||
// Initial sync attempt
|
||||
if (navigator.onLine) sync();
|
||||
|
||||
return () => {
|
||||
@ -72,5 +75,28 @@ export function SyncProvider({ children }: { children: ReactNode }) {
|
||||
};
|
||||
}, [sync, refreshCounts]);
|
||||
|
||||
return <SyncCtx.Provider value={state}>{children}</SyncCtx.Provider>;
|
||||
return (
|
||||
<SyncCtx.Provider value={state}>
|
||||
{children}
|
||||
{failedIds.length > 0 && (
|
||||
<div className="fixed bottom-4 left-4 right-4 z-50 flex flex-col gap-2">
|
||||
{failedIds.map((id) => (
|
||||
<div
|
||||
key={id}
|
||||
className="flex items-center justify-between rounded-lg bg-destructive px-4 py-3 text-sm text-destructive-foreground shadow-lg"
|
||||
>
|
||||
<span>Pedido {id.slice(0, 8)}… falhou — contacta o supervisor.</span>
|
||||
<button
|
||||
onClick={() => setFailedIds((prev) => prev.filter((x) => x !== id))}
|
||||
className="ml-4 shrink-0 opacity-80 hover:opacity-100"
|
||||
aria-label="Fechar"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</SyncCtx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
(()=>{"use strict";self.onmessage=async e=>{switch(e.data.type){case"__START_URL_CACHE__":{let t=e.data.url,a=await fetch(t);if(!a.redirected)return(await caches.open("start-url")).put(t,a);return Promise.resolve()}case"__FRONTEND_NAV_CACHE__":{let t=e.data.url,a=await caches.open("pages");if(await a.match(t,{ignoreSearch:!0}))return;let s=await fetch(t);if(!s.ok)return;if(a.put(t,s.clone()),e.data.shouldCacheAggressively&&s.headers.get("Content-Type")?.includes("text/html"))try{let e=await s.text(),t=[],a=await caches.open("static-style-assets"),r=await caches.open("next-static-js-assets"),c=await caches.open("static-js-assets");for(let[s,r]of e.matchAll(/<link.*?href=['"](.*?)['"].*?>/g))/rel=['"]stylesheet['"]/.test(s)&&t.push(a.match(r).then(e=>e?Promise.resolve():a.add(r)));for(let[,a]of e.matchAll(/<script.*?src=['"](.*?)['"].*?>/g)){let e=/\/_next\/static.+\.js$/i.test(a)?r:c;t.push(e.match(a).then(t=>t?Promise.resolve():e.add(a)))}return await Promise.all(t)}catch{}return Promise.resolve()}default:return Promise.resolve()}}})();
|
||||
102
apps/operator-pwa/public/swe-worker-development.js
Normal file
102
apps/operator-pwa/public/swe-worker-development.js
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
/******/ (() => { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
/******/ var __webpack_modules__ = ({
|
||||
|
||||
/***/ "../../node_modules/.pnpm/@ducanh2912+next-pwa@10.2.9_1d3731f6205d164bcee9533e20d53672/node_modules/@ducanh2912/next-pwa/dist/sw-entry-worker.js":
|
||||
/*!*******************************************************************************************************************************************************!*\
|
||||
!*** ../../node_modules/.pnpm/@ducanh2912+next-pwa@10.2.9_1d3731f6205d164bcee9533e20d53672/node_modules/@ducanh2912/next-pwa/dist/sw-entry-worker.js ***!
|
||||
\*******************************************************************************************************************************************************/
|
||||
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\nself.onmessage = async (e)=>{\n switch(e.data.type){\n case \"__START_URL_CACHE__\":\n {\n let t = e.data.url, a = await fetch(t);\n if (!a.redirected) return (await caches.open(\"start-url\")).put(t, a);\n return Promise.resolve();\n }\n case \"__FRONTEND_NAV_CACHE__\":\n {\n let t = e.data.url, a = await caches.open(\"pages\");\n if (await a.match(t, {\n ignoreSearch: !0\n })) return;\n let s = await fetch(t);\n if (!s.ok) return;\n if (a.put(t, s.clone()), e.data.shouldCacheAggressively && s.headers.get(\"Content-Type\")?.includes(\"text/html\")) try {\n let e = await s.text(), t = [], a = await caches.open(\"static-style-assets\"), r = await caches.open(\"next-static-js-assets\"), c = await caches.open(\"static-js-assets\");\n for (let [s, r] of e.matchAll(/<link.*?href=['\"](.*?)['\"].*?>/g))/rel=['\"]stylesheet['\"]/.test(s) && t.push(a.match(r).then((e)=>e ? Promise.resolve() : a.add(r)));\n for (let [, a] of e.matchAll(/<script.*?src=['\"](.*?)['\"].*?>/g)){\n let e = /\\/_next\\/static.+\\.js$/i.test(a) ? r : c;\n t.push(e.match(a).then((t)=>t ? Promise.resolve() : e.add(a)));\n }\n return await Promise.all(t);\n } catch {}\n return Promise.resolve();\n }\n default:\n return Promise.resolve();\n }\n};//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi4vLi4vbm9kZV9tb2R1bGVzLy5wbnBtL0BkdWNhbmgyOTEyK25leHQtcHdhQDEwLjIuOV8xZDM3MzFmNjIwNWQxNjRiY2VlOTUzM2UyMGQ1MzY3Mi9ub2RlX21vZHVsZXMvQGR1Y2FuaDI5MTIvbmV4dC1wd2EvZGlzdC9zdy1lbnRyeS13b3JrZXIuanMiLCJtYXBwaW5ncyI6IjtBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsaUJBQWlCO0FBQ2pCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0Esa0JBQWtCO0FBQ2xCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsiQzpcXFVzZXJzXFxwcmRjZ1xcRG9jdW1lbnRzXFxHaXRcXEZpZWxkT3BzXFxub2RlX21vZHVsZXNcXC5wbnBtXFxAZHVjYW5oMjkxMituZXh0LXB3YUAxMC4yLjlfMWQzNzMxZjYyMDVkMTY0YmNlZTk1MzNlMjBkNTM2NzJcXG5vZGVfbW9kdWxlc1xcQGR1Y2FuaDI5MTJcXG5leHQtcHdhXFxkaXN0XFxzdy1lbnRyeS13b3JrZXIuanMiXSwic291cmNlc0NvbnRlbnQiOlsic2VsZi5vbm1lc3NhZ2UgPSBhc3luYyAoZSk9PntcbiAgICBzd2l0Y2goZS5kYXRhLnR5cGUpe1xuICAgICAgICBjYXNlIFwiX19TVEFSVF9VUkxfQ0FDSEVfX1wiOlxuICAgICAgICAgICAge1xuICAgICAgICAgICAgICAgIGxldCB0ID0gZS5kYXRhLnVybCwgYSA9IGF3YWl0IGZldGNoKHQpO1xuICAgICAgICAgICAgICAgIGlmICghYS5yZWRpcmVjdGVkKSByZXR1cm4gKGF3YWl0IGNhY2hlcy5vcGVuKFwic3RhcnQtdXJsXCIpKS5wdXQodCwgYSk7XG4gICAgICAgICAgICAgICAgcmV0dXJuIFByb21pc2UucmVzb2x2ZSgpO1xuICAgICAgICAgICAgfVxuICAgICAgICBjYXNlIFwiX19GUk9OVEVORF9OQVZfQ0FDSEVfX1wiOlxuICAgICAgICAgICAge1xuICAgICAgICAgICAgICAgIGxldCB0ID0gZS5kYXRhLnVybCwgYSA9IGF3YWl0IGNhY2hlcy5vcGVuKFwicGFnZXNcIik7XG4gICAgICAgICAgICAgICAgaWYgKGF3YWl0IGEubWF0Y2godCwge1xuICAgICAgICAgICAgICAgICAgICBpZ25vcmVTZWFyY2g6ICEwXG4gICAgICAgICAgICAgICAgfSkpIHJldHVybjtcbiAgICAgICAgICAgICAgICBsZXQgcyA9IGF3YWl0IGZldGNoKHQpO1xuICAgICAgICAgICAgICAgIGlmICghcy5vaykgcmV0dXJuO1xuICAgICAgICAgICAgICAgIGlmIChhLnB1dCh0LCBzLmNsb25lKCkpLCBlLmRhdGEuc2hvdWxkQ2FjaGVBZ2dyZXNzaXZlbHkgJiYgcy5oZWFkZXJzLmdldChcIkNvbnRlbnQtVHlwZVwiKT8uaW5jbHVkZXMoXCJ0ZXh0L2h0bWxcIikpIHRyeSB7XG4gICAgICAgICAgICAgICAgICAgIGxldCBlID0gYXdhaXQgcy50ZXh0KCksIHQgPSBbXSwgYSA9IGF3YWl0IGNhY2hlcy5vcGVuKFwic3RhdGljLXN0eWxlLWFzc2V0c1wiKSwgciA9IGF3YWl0IGNhY2hlcy5vcGVuKFwibmV4dC1zdGF0aWMtanMtYXNzZXRzXCIpLCBjID0gYXdhaXQgY2FjaGVzLm9wZW4oXCJzdGF0aWMtanMtYXNzZXRzXCIpO1xuICAgICAgICAgICAgICAgICAgICBmb3IgKGxldCBbcywgcl0gb2YgZS5tYXRjaEFsbCgvPGxpbmsuKj9ocmVmPVsnXCJdKC4qPylbJ1wiXS4qPz4vZykpL3JlbD1bJ1wiXXN0eWxlc2hlZXRbJ1wiXS8udGVzdChzKSAmJiB0LnB1c2goYS5tYXRjaChyKS50aGVuKChlKT0+ZSA/IFByb21pc2UucmVzb2x2ZSgpIDogYS5hZGQocikpKTtcbiAgICAgICAgICAgICAgICAgICAgZm9yIChsZXQgWywgYV0gb2YgZS5tYXRjaEFsbCgvPHNjcmlwdC4qP3NyYz1bJ1wiXSguKj8pWydcIl0uKj8+L2cpKXtcbiAgICAgICAgICAgICAgICAgICAgICAgIGxldCBlID0gL1xcL19uZXh0XFwvc3RhdGljLitcXC5qcyQvaS50ZXN0KGEpID8gciA6IGM7XG4gICAgICAgICAgICAgICAgICAgICAgICB0LnB1c2goZS5tYXRjaChhKS50aGVuKCh0KT0+dCA/IFByb21pc2UucmVzb2x2ZSgpIDogZS5hZGQoYSkpKTtcbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgICAgICByZXR1cm4gYXdhaXQgUHJvbWlzZS5hbGwodCk7XG4gICAgICAgICAgICAgICAgfSBjYXRjaCAge31cbiAgICAgICAgICAgICAgICByZXR1cm4gUHJvbWlzZS5yZXNvbHZlKCk7XG4gICAgICAgICAgICB9XG4gICAgICAgIGRlZmF1bHQ6XG4gICAgICAgICAgICByZXR1cm4gUHJvbWlzZS5yZXNvbHZlKCk7XG4gICAgfVxufTsiXSwibmFtZXMiOltdLCJpZ25vcmVMaXN0IjpbMF0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///../../node_modules/.pnpm/@ducanh2912+next-pwa@10.2.9_1d3731f6205d164bcee9533e20d53672/node_modules/@ducanh2912/next-pwa/dist/sw-entry-worker.js\n"));
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
||||
/************************************************************************/
|
||||
/******/ // The require scope
|
||||
/******/ var __webpack_require__ = {};
|
||||
/******/
|
||||
/************************************************************************/
|
||||
/******/ /* webpack/runtime/make namespace object */
|
||||
/******/ (() => {
|
||||
/******/ // define __esModule on exports
|
||||
/******/ __webpack_require__.r = (exports) => {
|
||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||
/******/ }
|
||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/trusted types policy */
|
||||
/******/ (() => {
|
||||
/******/ var policy;
|
||||
/******/ __webpack_require__.tt = () => {
|
||||
/******/ // Create Trusted Type policy if Trusted Types are available and the policy doesn't exist yet.
|
||||
/******/ if (policy === undefined) {
|
||||
/******/ policy = {
|
||||
/******/ createScript: (script) => (script)
|
||||
/******/ };
|
||||
/******/ if (typeof trustedTypes !== "undefined" && trustedTypes.createPolicy) {
|
||||
/******/ policy = trustedTypes.createPolicy("nextjs#bundler", policy);
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ return policy;
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/trusted types script */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.ts = (script) => (__webpack_require__.tt().createScript(script));
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/react refresh */
|
||||
/******/ (() => {
|
||||
/******/ if (__webpack_require__.i) {
|
||||
/******/ __webpack_require__.i.push((options) => {
|
||||
/******/ const originalFactory = options.factory;
|
||||
/******/ options.factory = (moduleObject, moduleExports, webpackRequire) => {
|
||||
/******/ const hasRefresh = typeof self !== "undefined" && !!self.$RefreshInterceptModuleExecution$;
|
||||
/******/ const cleanup = hasRefresh ? self.$RefreshInterceptModuleExecution$(moduleObject.id) : () => {};
|
||||
/******/ try {
|
||||
/******/ originalFactory.call(this, moduleObject, moduleExports, webpackRequire);
|
||||
/******/ } finally {
|
||||
/******/ cleanup();
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ })
|
||||
/******/ }
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/compat */
|
||||
/******/
|
||||
/******/
|
||||
/******/ // noop fns to prevent runtime errors during initialization
|
||||
/******/ if (typeof self !== "undefined") {
|
||||
/******/ self.$RefreshReg$ = function () {};
|
||||
/******/ self.$RefreshSig$ = function () {
|
||||
/******/ return function (type) {
|
||||
/******/ return type;
|
||||
/******/ };
|
||||
/******/ };
|
||||
/******/ }
|
||||
/******/
|
||||
/************************************************************************/
|
||||
/******/
|
||||
/******/ // startup
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ // This entry module can't be inlined because the eval-source-map devtool is used.
|
||||
/******/ var __webpack_exports__ = {};
|
||||
/******/ __webpack_modules__["../../node_modules/.pnpm/@ducanh2912+next-pwa@10.2.9_1d3731f6205d164bcee9533e20d53672/node_modules/@ducanh2912/next-pwa/dist/sw-entry-worker.js"](0, __webpack_exports__, __webpack_require__);
|
||||
/******/
|
||||
/******/ })()
|
||||
;
|
||||
2455
apps/operator-pwa/public/workbox-5147176e.js
Normal file
2455
apps/operator-pwa/public/workbox-5147176e.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -1,17 +1,19 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
const PORT = 3000;
|
||||
const BASE_URL = `http://localhost:${PORT}`;
|
||||
const OPERATOR_URL = 'http://localhost:3000';
|
||||
const ADMIN_URL = 'http://localhost:3001';
|
||||
|
||||
export const ADMIN_BASE = ADMIN_URL;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
fullyParallel: true,
|
||||
fullyParallel: false, // sequential — tests share state (DB, MinIO)
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
workers: 1,
|
||||
reporter: [['list'], ['html', { open: 'never' }]],
|
||||
use: {
|
||||
baseURL: BASE_URL,
|
||||
baseURL: OPERATOR_URL,
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
@ -21,20 +23,26 @@ export default defineConfig({
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
// Run from the repo root so workspace resolution works.
|
||||
webServer: [
|
||||
{
|
||||
command: 'pnpm --filter @repo/operator-pwa dev',
|
||||
cwd: '..',
|
||||
url: BASE_URL,
|
||||
url: OPERATOR_URL,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
// Force the dev autologin on for E2E regardless of the developer's local
|
||||
// .env. This env applies only to the child dev server, not the test
|
||||
// process itself.
|
||||
env: {
|
||||
AUTH_DEV_AUTOLOGIN: 'true',
|
||||
env: { AUTH_DEV_AUTOLOGIN: 'true' },
|
||||
},
|
||||
{
|
||||
command: 'pnpm --filter @repo/admin-web dev',
|
||||
cwd: '..',
|
||||
url: ADMIN_URL,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
env: { AUTH_DEV_AUTOLOGIN: 'true' },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
74
e2e/tests/mai-call.spec.ts
Normal file
74
e2e/tests/mai-call.spec.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { ADMIN_BASE } from '../playwright.config';
|
||||
|
||||
/**
|
||||
* Happy-path E2E for MAI CALL v0.1:
|
||||
* operator creates request → admin claims → admin resolves
|
||||
*
|
||||
* Preconditions (met by `pnpm db:seed` + running docker compose):
|
||||
* - Demo Factory tenant with workstations CTR04, QVN_RTL_2, MTG_01
|
||||
* - admin@demo.local with role ADMIN
|
||||
* - AUTH_DEV_AUTOLOGIN=true on both servers (set in playwright.config.ts)
|
||||
*/
|
||||
test('MAI CALL happy path: create → claim → resolve', async ({ page, context }) => {
|
||||
// Use a unique description so the test can find its own card in the queue.
|
||||
const desc = `E2E ${Date.now()} — ruído anormal no posto`;
|
||||
|
||||
// ── 1. Operator creates a request ────────────────────────────────────────
|
||||
await page.goto('/maintenance/new');
|
||||
|
||||
// Wait for workstation options to load (select is disabled while loading)
|
||||
await expect(page.locator('#workstation')).toBeEnabled({ timeout: 15_000 });
|
||||
|
||||
// Select the first real workstation
|
||||
const firstOpt = page.locator('#workstation option:not([value=""])').first();
|
||||
const wsValue = await firstOpt.getAttribute('value');
|
||||
await page.selectOption('#workstation', wsValue!);
|
||||
|
||||
await page.fill('#description', desc);
|
||||
await page.click('button[type=submit]');
|
||||
|
||||
// ── 2. Wait for the sync to complete ─────────────────────────────────────
|
||||
// The form queues to IndexedDB; the SyncProvider immediately syncs when
|
||||
// online. The sent page changes from "Em fila" → "Pedido enviado".
|
||||
await page.waitForURL('**/maintenance/sent**');
|
||||
await expect(page.locator('h1')).toHaveText('Pedido enviado', { timeout: 30_000 });
|
||||
|
||||
// ── 3. Admin queue shows the request ─────────────────────────────────────
|
||||
const adminPage = await context.newPage();
|
||||
await adminPage.goto(`${ADMIN_BASE}/maintenance`);
|
||||
|
||||
// Find the card that contains our description (admin polls every 5s)
|
||||
const card = adminPage
|
||||
.locator('[data-testid="request-card"]')
|
||||
.filter({ hasText: desc.slice(0, 40) });
|
||||
await expect(card).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Card starts as OPEN
|
||||
await expect(card.locator('span', { hasText: 'Aberto' })).toBeVisible();
|
||||
|
||||
// ── 4. Admin claims the request ───────────────────────────────────────────
|
||||
await card.locator('button', { hasText: 'Aceitar' }).click();
|
||||
|
||||
// Card changes to CLAIMED
|
||||
await expect(card.locator('span', { hasText: 'Em curso' })).toBeVisible({ timeout: 10_000 });
|
||||
await expect(card.locator('button', { hasText: 'Aceitar' })).not.toBeVisible();
|
||||
|
||||
// ── 5. Enable RESOLVED filter so the card stays visible after resolve ─────
|
||||
await adminPage.locator('label', { hasText: 'Resolvido' }).click();
|
||||
|
||||
// ── 6. Admin resolves the request ────────────────────────────────────────
|
||||
await card.locator('button', { hasText: 'Marcar resolvido' }).click();
|
||||
|
||||
const dialog = adminPage.locator('h2', { hasText: 'Marcar como resolvido' });
|
||||
await expect(dialog).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
await adminPage.locator('button', { hasText: 'Confirmar' }).click();
|
||||
|
||||
// Card changes to RESOLVED
|
||||
await expect(card.locator('span', { hasText: 'Resolvido' })).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// No action buttons remain on RESOLVED card
|
||||
await expect(card.locator('button', { hasText: 'Aceitar' })).not.toBeVisible();
|
||||
await expect(card.locator('button', { hasText: 'Marcar resolvido' })).not.toBeVisible();
|
||||
});
|
||||
@ -1,18 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('ping smoke', () => {
|
||||
test('home page shows the seeded Demo Factory tenant', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// RSC card with the server-side ping result.
|
||||
const success = page.getByTestId('ping-success');
|
||||
await expect(success).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
const tenant = page.getByTestId('tenant-name');
|
||||
await expect(tenant).toHaveText('Demo Factory');
|
||||
|
||||
// Client-side hook hitting the same procedure via /api/trpc.
|
||||
const clientTenant = page.getByTestId('ping-client-tenant');
|
||||
await expect(clientTenant).toContainText('Demo Factory');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user