@owlmeans/web-panel 0.1.18-rc.56 → 0.1.18-rc.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-panel/SKILL.md +18 -8
- package/build/components/socket/i18n/be.json +4 -2
- package/build/components/socket/i18n/de.json +4 -2
- package/build/components/socket/i18n/en.json +4 -2
- package/build/components/socket/i18n/es.json +4 -2
- package/build/components/socket/i18n/fr.json +4 -2
- package/build/components/socket/i18n/pl.json +4 -2
- package/build/components/socket/i18n/ru.json +4 -2
- package/build/components/socket/i18n/uk.json +4 -2
- package/build/components/socket/reload-dialog.d.ts +8 -4
- package/build/components/socket/reload-dialog.d.ts.map +1 -1
- package/build/components/socket/reload-dialog.js +15 -7
- package/build/components/socket/reload-dialog.js.map +1 -1
- package/build/types.d.ts +1 -1
- package/package.json +29 -29
- package/src/components/socket/i18n/be.json +4 -2
- package/src/components/socket/i18n/de.json +4 -2
- package/src/components/socket/i18n/en.json +4 -2
- package/src/components/socket/i18n/es.json +4 -2
- package/src/components/socket/i18n/fr.json +4 -2
- package/src/components/socket/i18n/pl.json +4 -2
- package/src/components/socket/i18n/ru.json +4 -2
- package/src/components/socket/i18n/uk.json +4 -2
- package/src/components/socket/reload-dialog.tsx +23 -7
- package/src/types.ts +1 -1
- package/tests/harness/mount.tsx +13 -1
- package/tests/socket-dialog.spec.ts +85 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ but never started.
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
bun add @owlmeans/web-panel@^0.1.18-rc.
|
|
14
|
+
bun add @owlmeans/web-panel@^0.1.18-rc.58
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Peer requirements (the consuming app provides these): `react`, `react-dom`,
|
|
@@ -86,7 +86,7 @@ import { commonConfig, MY_APP_WEB } from 'my-app-common'
|
|
|
86
86
|
import type { Config } from './types.js'
|
|
87
87
|
|
|
88
88
|
const cfg: Config = config(MY_APP_WEB, commonConfig as Config)
|
|
89
|
-
// Block the screen with a "reload the page" prompt once
|
|
89
|
+
// Block the screen with a "try again / reload the page" prompt once a socket has given up reconnecting.
|
|
90
90
|
cfg.socket = { ...cfg.socket, reloadDialog: true }
|
|
91
91
|
|
|
92
92
|
export default cfg
|
|
@@ -496,7 +496,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
496
496
|
your project's skill store (`.agents/skills/`):
|
|
497
497
|
|
|
498
498
|
```sh
|
|
499
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
499
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.35
|
|
500
500
|
```
|
|
501
501
|
|
|
502
502
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/web-panel",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.58",
|
|
5
|
+
"generatedAt": "2026-09-22T21:57:18.836Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/web-panel
|
|
9
9
|
|
|
10
10
|
**Layer:** Web (React)
|
|
11
|
-
**Install:** `"@owlmeans/web-panel": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/web-panel": "^0.1.18-rc.58"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
|
@@ -19,7 +19,7 @@ user-invocable: false
|
|
|
19
19
|
| `TopNav` / `SideNav` / `MobileNav` / `Footer` | The shell's pieces, mountable on their own |
|
|
20
20
|
| `Toaster` | The application's toast surface — mounted once, in the layout |
|
|
21
21
|
| `ThemeToggle` / `useColorScheme` | The light/dark switcher (`ThemeToggleProps`, `ThemeToggleLabels`) and the hook behind it (`ColorSchemeModel`) — see *Light and dark* below |
|
|
22
|
-
| `SocketReloadDialog` | The global "
|
|
22
|
+
| `SocketReloadDialog` | The global "connection lost — try again / reload" prompt (opt in via `cfg.socket.reloadDialog`) `PanelApp` mounts automatically — see below. The hooks it reads, `useSocketStatus` and `useSocketRetry`, are imported from `@owlmeans/client-socket`, not from this package |
|
|
23
23
|
| `Link` | An `<a>` addressing an entrypoint alias (or a literal `src`), with the label taken from i18n |
|
|
24
24
|
| `LoginScreen` / `LocalizedLoginScreen` / `appendLoginScreen` | The identity-provider choice screen — see `login-methods`. `LoginScreen` is pure w.r.t. `locale` too, exactly like `translate`: it is a prop, defaulting to nothing, never an implicit `useLanguage()` read, because a component that reaches for i18n context directly crashes an app mounted without one. `LocalizedLoginScreen` supplies `useLanguage()`'s value when the caller does not pass its own |
|
|
25
25
|
| `render(context, opts?)` | Mounts the tree inside `PanelApp`, with the browser language detector installed on the i18n instance. `opts` is `RenderOptions` plus `rootClassName` |
|
|
@@ -341,12 +341,22 @@ on every navigation. Neither does anything unless an app opts in:
|
|
|
341
341
|
cfg.socket = { reloadDialog: true }
|
|
342
342
|
```
|
|
343
343
|
|
|
344
|
-
Once
|
|
344
|
+
Once any `ws()`/`useWs()` connection in the app has exhausted its own retry budget
|
|
345
345
|
(`useSocketStatus() === 'lost'`), a global, blocking `AlertDialog` covers the screen — no Escape,
|
|
346
|
-
no outside click,
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
(`
|
|
346
|
+
no outside click, two actions:
|
|
347
|
+
|
|
348
|
+
- **"Try again"** (`data-socket-retry-action`) — `useSocketRetry().retry()` revives every lost
|
|
349
|
+
connection (see the `client-socket` skill). The dialog stays up with the button disabled and
|
|
350
|
+
reading "Reconnecting…" while they retry, closes once the aggregate is `'online'`, and offers
|
|
351
|
+
both buttons again if it falls back to `'lost'`.
|
|
352
|
+
- **"Reload page"** (`data-socket-reload-action`) — `window.location.reload()`, the fallback that
|
|
353
|
+
always works.
|
|
354
|
+
|
|
355
|
+
The status service also retries by itself when the tab becomes visible, the window gains focus or
|
|
356
|
+
the browser comes back online, so a socket that died in a background tab is usually back before
|
|
357
|
+
anyone presses anything — an open dialog shows "Reconnecting…" through that too. Strings are
|
|
358
|
+
lib-tier (`useI18nLib('socket', 'reload')`: `title`, `description`, `action`, `retry`,
|
|
359
|
+
`retrying`), 8 languages, under `src/components/socket/i18n/`.
|
|
350
360
|
|
|
351
361
|
Leave `cfg.socket.reloadDialog` unset (or `false`) for an app that would rather show its own
|
|
352
362
|
inline "reconnecting…" state — `useSocketStatus()` from `@owlmeans/client-socket` serves that,
|
|
@@ -536,7 +546,7 @@ in a linked workspace, so it is the reliable scan target in both modes.
|
|
|
536
546
|
## Depends On
|
|
537
547
|
|
|
538
548
|
- `@owlmeans/web-client`, `@owlmeans/client-panel`, `@owlmeans/client-i18n`, `@owlmeans/web-router`
|
|
539
|
-
- `@owlmeans/client-socket` — `appendSocketStatus`, `useSocketStatus`, behind `SocketReloadDialog`
|
|
549
|
+
- `@owlmeans/client-socket` — `appendSocketStatus`, `useSocketStatus`, `useSocketRetry`, behind `SocketReloadDialog`
|
|
540
550
|
- Peers (app-provided): `react`, `react-dom`, `react-hook-form`, `tailwindcss`, `tailwind-merge`,
|
|
541
551
|
`clsx`, `class-variance-authority`, `lucide-react`, `ajv`, and the `@radix-ui/react-*` primitives
|
|
542
552
|
(`alert-dialog`, `dialog`, `label`, `navigation-menu`, `progress`, `separator`, `slot`). No MUI,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "Злучэнне страчана",
|
|
4
|
-
"description": "Не ўдалося аднавіць злучэнне пасля некалькіх спробаў.
|
|
5
|
-
"action": "Перазагрузіць старонку"
|
|
4
|
+
"description": "Не ўдалося аднавіць злучэнне пасля некалькіх спробаў. Паспрабуйце яшчэ раз або перазагрузіце старонку, каб працягнуць.",
|
|
5
|
+
"action": "Перазагрузіць старонку",
|
|
6
|
+
"retry": "Паспрабаваць яшчэ раз",
|
|
7
|
+
"retrying": "Паўторнае падключэнне…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "Verbindung verloren",
|
|
4
|
-
"description": "Die Verbindung konnte nach mehreren Versuchen nicht wiederhergestellt werden.
|
|
5
|
-
"action": "Seite neu laden"
|
|
4
|
+
"description": "Die Verbindung konnte nach mehreren Versuchen nicht wiederhergestellt werden. Versuche es erneut oder lade die Seite neu, um fortzufahren.",
|
|
5
|
+
"action": "Seite neu laden",
|
|
6
|
+
"retry": "Erneut versuchen",
|
|
7
|
+
"retrying": "Verbindung wird wiederhergestellt…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "Connection lost",
|
|
4
|
-
"description": "We couldn't restore the connection after several attempts.
|
|
5
|
-
"action": "Reload page"
|
|
4
|
+
"description": "We couldn't restore the connection after several attempts. Try again, or reload the page to continue.",
|
|
5
|
+
"action": "Reload page",
|
|
6
|
+
"retry": "Try again",
|
|
7
|
+
"retrying": "Reconnecting…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "Conexión perdida",
|
|
4
|
-
"description": "No se pudo restablecer la conexión tras varios intentos.
|
|
5
|
-
"action": "Recargar página"
|
|
4
|
+
"description": "No se pudo restablecer la conexión tras varios intentos. Vuelve a intentarlo o recarga la página para continuar.",
|
|
5
|
+
"action": "Recargar página",
|
|
6
|
+
"retry": "Reintentar",
|
|
7
|
+
"retrying": "Reconectando…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "Connexion perdue",
|
|
4
|
-
"description": "Nous n’avons pas pu rétablir la connexion après plusieurs tentatives.
|
|
5
|
-
"action": "Actualiser la page"
|
|
4
|
+
"description": "Nous n’avons pas pu rétablir la connexion après plusieurs tentatives. Réessayez ou actualisez la page pour continuer.",
|
|
5
|
+
"action": "Actualiser la page",
|
|
6
|
+
"retry": "Réessayer",
|
|
7
|
+
"retrying": "Reconnexion…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "Utracono połączenie",
|
|
4
|
-
"description": "Nie udało się przywrócić połączenia mimo kilku prób.
|
|
5
|
-
"action": "Odśwież stronę"
|
|
4
|
+
"description": "Nie udało się przywrócić połączenia mimo kilku prób. Spróbuj ponownie lub odśwież stronę, aby kontynuować.",
|
|
5
|
+
"action": "Odśwież stronę",
|
|
6
|
+
"retry": "Spróbuj ponownie",
|
|
7
|
+
"retrying": "Ponowne łączenie…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "Соединение потеряно",
|
|
4
|
-
"description": "Не удалось восстановить соединение после нескольких попыток.
|
|
5
|
-
"action": "Перезагрузить страницу"
|
|
4
|
+
"description": "Не удалось восстановить соединение после нескольких попыток. Попробуйте снова или перезагрузите страницу, чтобы продолжить.",
|
|
5
|
+
"action": "Перезагрузить страницу",
|
|
6
|
+
"retry": "Попробовать снова",
|
|
7
|
+
"retrying": "Переподключение…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "З'єднання втрачено",
|
|
4
|
-
"description": "Не вдалося відновити з'єднання після кількох спроб.
|
|
5
|
-
"action": "Перезавантажити сторінку"
|
|
4
|
+
"description": "Не вдалося відновити з'єднання після кількох спроб. Спробуйте ще раз або перезавантажте сторінку, щоб продовжити.",
|
|
5
|
+
"action": "Перезавантажити сторінку",
|
|
6
|
+
"retry": "Спробувати ще раз",
|
|
7
|
+
"retrying": "Перепідключення…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { FC } from 'react';
|
|
2
2
|
/**
|
|
3
|
-
* A global, blocking "
|
|
3
|
+
* A global, blocking "connection lost" prompt — opt in per app with `cfg.socket.reloadDialog`.
|
|
4
4
|
*
|
|
5
5
|
* Mount it once, above the router (`PanelApp` does), exactly like `PanelCookieConsent`: a dialog
|
|
6
6
|
* mounted inside a route is torn down on every navigation, and this one is meant to survive them
|
|
7
7
|
* all. It opens the moment `useSocketStatus()` reports `'lost'` — every `ws()`/`useWs()`
|
|
8
|
-
* connection in the app has exhausted its own retry budget (10 minutes by default)
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* connection in the app has exhausted its own retry budget (10 minutes by default).
|
|
9
|
+
*
|
|
10
|
+
* Two exits: "Try again" revives every lost connection (`useSocketRetry()`), and the dialog stays
|
|
11
|
+
* up showing progress until they are back online — or offers both buttons again if they are lost
|
|
12
|
+
* again. "Reload page" is the fallback that always works. The status service also retries by
|
|
13
|
+
* itself when the tab or window becomes active, so a socket that died in a background tab is
|
|
14
|
+
* usually back before anyone has to press anything.
|
|
11
15
|
*
|
|
12
16
|
* Non-dismissible on two levels: Radix's `AlertDialogContent` (unlike its plain `Dialog` sibling)
|
|
13
17
|
* never treats an outside click as a request to close, so there is no `onPointerDownOutside` to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reload-dialog.d.ts","sourceRoot":"","sources":["../../../src/components/socket/reload-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"reload-dialog.d.ts","sourceRoot":"","sources":["../../../src/components/socket/reload-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAS/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,kBAAkB,EAAE,EAgChC,CAAA"}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useSocketStatus } from '@owlmeans/client-socket';
|
|
2
|
+
import { useSocketRetry, useSocketStatus } from '@owlmeans/client-socket';
|
|
3
3
|
import { useI18nLib } from '@owlmeans/client-i18n';
|
|
4
4
|
import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '../../@/components/ui/alert-dialog.js';
|
|
5
|
+
import { Button } from '../../@/components/ui/button.js';
|
|
5
6
|
/**
|
|
6
|
-
* A global, blocking "
|
|
7
|
+
* A global, blocking "connection lost" prompt — opt in per app with `cfg.socket.reloadDialog`.
|
|
7
8
|
*
|
|
8
9
|
* Mount it once, above the router (`PanelApp` does), exactly like `PanelCookieConsent`: a dialog
|
|
9
10
|
* mounted inside a route is torn down on every navigation, and this one is meant to survive them
|
|
10
11
|
* all. It opens the moment `useSocketStatus()` reports `'lost'` — every `ws()`/`useWs()`
|
|
11
|
-
* connection in the app has exhausted its own retry budget (10 minutes by default)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* connection in the app has exhausted its own retry budget (10 minutes by default).
|
|
13
|
+
*
|
|
14
|
+
* Two exits: "Try again" revives every lost connection (`useSocketRetry()`), and the dialog stays
|
|
15
|
+
* up showing progress until they are back online — or offers both buttons again if they are lost
|
|
16
|
+
* again. "Reload page" is the fallback that always works. The status service also retries by
|
|
17
|
+
* itself when the tab or window becomes active, so a socket that died in a background tab is
|
|
18
|
+
* usually back before anyone has to press anything.
|
|
14
19
|
*
|
|
15
20
|
* Non-dismissible on two levels: Radix's `AlertDialogContent` (unlike its plain `Dialog` sibling)
|
|
16
21
|
* never treats an outside click as a request to close, so there is no `onPointerDownOutside` to
|
|
@@ -19,8 +24,11 @@ import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescript
|
|
|
19
24
|
*/
|
|
20
25
|
export const SocketReloadDialog = () => {
|
|
21
26
|
const status = useSocketStatus();
|
|
27
|
+
const { retry, retrying } = useSocketRetry();
|
|
22
28
|
const t = useI18nLib('socket', 'reload');
|
|
23
|
-
const open = status === 'lost';
|
|
24
|
-
|
|
29
|
+
const open = status === 'lost' || (retrying && status === 'reconnecting');
|
|
30
|
+
// Closing still animates the content out — keep the retry in its busy state until it is gone.
|
|
31
|
+
const busy = retrying || !open;
|
|
32
|
+
return _jsx(AlertDialog, { open: open, children: _jsxs(AlertDialogContent, { "data-socket-reload-dialog": true, onEscapeKeyDown: event => event.preventDefault(), children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: t('title') }), _jsx(AlertDialogDescription, { children: t('description') })] }), _jsxs(AlertDialogFooter, { children: [_jsx(Button, { variant: "outline", "data-socket-reload-action": true, onClick: () => window.location.reload(), children: t('action') }), _jsx(AlertDialogAction, { "data-socket-retry-action": true, disabled: busy, "aria-busy": busy, onClick: retry, children: busy ? t('retrying') : t('retry') })] })] }) });
|
|
25
33
|
};
|
|
26
34
|
//# sourceMappingURL=reload-dialog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reload-dialog.js","sourceRoot":"","sources":["../../../src/components/socket/reload-dialog.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"reload-dialog.js","sourceRoot":"","sources":["../../../src/components/socket/reload-dialog.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AACzE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAClD,OAAO,EACL,WAAW,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,sBAAsB,EAC1E,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EACvD,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,iCAAiC,CAAA;AAExD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAO,GAAG,EAAE;IACzC,MAAM,MAAM,GAAG,eAAe,EAAE,CAAA;IAChC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,cAAc,EAAE,CAAA;IAC5C,MAAM,CAAC,GAAG,UAAU,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;IACxC,MAAM,IAAI,GAAG,MAAM,KAAK,MAAM,IAAI,CAAC,QAAQ,IAAI,MAAM,KAAK,cAAc,CAAC,CAAA;IACzE,8FAA8F;IAC9F,MAAM,IAAI,GAAG,QAAQ,IAAI,CAAC,IAAI,CAAA;IAE9B,OAAO,KAAC,WAAW,IAAC,IAAI,EAAE,IAAI,YAC5B,MAAC,kBAAkB,uCAEjB,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,cAAc,EAAE,aAEhD,MAAC,iBAAiB,eAChB,KAAC,gBAAgB,cAAE,CAAC,CAAC,OAAO,CAAC,GAAoB,EACjD,KAAC,sBAAsB,cAAE,CAAC,CAAC,aAAa,CAAC,GAA0B,IACjD,EACpB,MAAC,iBAAiB,eAChB,KAAC,MAAM,IAAC,OAAO,EAAC,SAAS,qCAA2B,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,YACxF,CAAC,CAAC,QAAQ,CAAC,GACL,EACT,KAAC,iBAAiB,sCAEhB,QAAQ,EAAE,IAAI,eACH,IAAI,EACf,OAAO,EAAE,KAAK,YAEb,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAChB,IACF,IACD,GACT,CAAA;AAChB,CAAC,CAAA"}
|
package/build/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { SocketClientSettings, SocketStatusServiceAppend } from '@owlmeans/
|
|
|
5
5
|
export interface AppConfig extends Config, WithFlowConfig {
|
|
6
6
|
socket?: SocketClientSettings & {
|
|
7
7
|
/**
|
|
8
|
-
* Show `SocketReloadDialog` — a global, blocking "reload the page" prompt — once every
|
|
8
|
+
* Show `SocketReloadDialog` — a global, blocking "try again / reload the page" prompt — once every
|
|
9
9
|
* socket in the app has given up reconnecting (`useSocketStatus() === 'lost'`). Off by
|
|
10
10
|
* default: an app opts in once it has decided that outcome deserves a full-page prompt
|
|
11
11
|
* rather than the caller's own fallback UI.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/web-panel",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.58",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -51,33 +51,33 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@hookform/resolvers": "^5.2.2",
|
|
54
|
-
"@owlmeans/api-config-client": "^0.1.18-rc.
|
|
55
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
56
|
-
"@owlmeans/auth-common": "^0.1.18-rc.
|
|
57
|
-
"@owlmeans/basic-envelope": "^0.1.18-rc.
|
|
58
|
-
"@owlmeans/client": "^0.1.18-rc.
|
|
59
|
-
"@owlmeans/client-auth": "^0.1.18-rc.
|
|
60
|
-
"@owlmeans/client-config": "^0.1.18-rc.
|
|
61
|
-
"@owlmeans/client-context": "^0.1.18-rc.
|
|
62
|
-
"@owlmeans/client-entrypoint": "^0.1.18-rc.
|
|
63
|
-
"@owlmeans/client-flow": "^0.1.18-rc.
|
|
64
|
-
"@owlmeans/client-i18n": "^0.1.18-rc.
|
|
65
|
-
"@owlmeans/client-panel": "^0.1.18-rc.
|
|
66
|
-
"@owlmeans/client-route": "^0.1.18-rc.
|
|
67
|
-
"@owlmeans/client-socket": "^0.1.18-rc.
|
|
68
|
-
"@owlmeans/config": "^0.1.18-rc.
|
|
69
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
70
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
71
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
72
|
-
"@owlmeans/flow": "^0.1.18-rc.
|
|
73
|
-
"@owlmeans/i18n": "^0.1.18-rc.
|
|
74
|
-
"@owlmeans/route": "^0.1.18-rc.
|
|
75
|
-
"@owlmeans/state": "^0.1.18-rc.
|
|
76
|
-
"@owlmeans/web-client": "^0.1.18-rc.
|
|
77
|
-
"@owlmeans/web-consent": "^0.1.18-rc.
|
|
78
|
-
"@owlmeans/web-db": "^0.1.18-rc.
|
|
79
|
-
"@owlmeans/web-flow": "^0.1.18-rc.
|
|
80
|
-
"@owlmeans/web-router": "^0.1.18-rc.
|
|
54
|
+
"@owlmeans/api-config-client": "^0.1.18-rc.33",
|
|
55
|
+
"@owlmeans/auth": "^0.1.18-rc.33",
|
|
56
|
+
"@owlmeans/auth-common": "^0.1.18-rc.35",
|
|
57
|
+
"@owlmeans/basic-envelope": "^0.1.18-rc.34",
|
|
58
|
+
"@owlmeans/client": "^0.1.18-rc.38",
|
|
59
|
+
"@owlmeans/client-auth": "^0.1.18-rc.45",
|
|
60
|
+
"@owlmeans/client-config": "^0.1.18-rc.35",
|
|
61
|
+
"@owlmeans/client-context": "^0.1.18-rc.36",
|
|
62
|
+
"@owlmeans/client-entrypoint": "^0.1.18-rc.35",
|
|
63
|
+
"@owlmeans/client-flow": "^0.1.18-rc.41",
|
|
64
|
+
"@owlmeans/client-i18n": "^0.1.18-rc.37",
|
|
65
|
+
"@owlmeans/client-panel": "^0.1.18-rc.45",
|
|
66
|
+
"@owlmeans/client-route": "^0.1.18-rc.36",
|
|
67
|
+
"@owlmeans/client-socket": "^0.1.18-rc.36",
|
|
68
|
+
"@owlmeans/config": "^0.1.18-rc.35",
|
|
69
|
+
"@owlmeans/context": "^0.1.18-rc.31",
|
|
70
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.34",
|
|
71
|
+
"@owlmeans/error": "^0.1.18-rc.33",
|
|
72
|
+
"@owlmeans/flow": "^0.1.18-rc.36",
|
|
73
|
+
"@owlmeans/i18n": "^0.1.18-rc.34",
|
|
74
|
+
"@owlmeans/route": "^0.1.18-rc.32",
|
|
75
|
+
"@owlmeans/state": "^0.1.18-rc.32",
|
|
76
|
+
"@owlmeans/web-client": "^0.1.18-rc.48",
|
|
77
|
+
"@owlmeans/web-consent": "^0.1.18-rc.29",
|
|
78
|
+
"@owlmeans/web-db": "^0.1.18-rc.36",
|
|
79
|
+
"@owlmeans/web-flow": "^0.1.18-rc.40",
|
|
80
|
+
"@owlmeans/web-router": "^0.1.18-rc.36",
|
|
81
81
|
"i18next-browser-languagedetector": "^8.0.0",
|
|
82
82
|
"qrcode.react": "^4.2.0",
|
|
83
83
|
"react-google-recaptcha": "^3.1.0",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
},
|
|
104
104
|
"devDependencies": {
|
|
105
105
|
"@owlmeans/dep-config": "workspace:*",
|
|
106
|
-
"@owlmeans/test-ui": "^0.1.18-rc.
|
|
106
|
+
"@owlmeans/test-ui": "^0.1.18-rc.36",
|
|
107
107
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
108
108
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
109
109
|
"@radix-ui/react-label": "^2.1.0",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "Злучэнне страчана",
|
|
4
|
-
"description": "Не ўдалося аднавіць злучэнне пасля некалькіх спробаў.
|
|
5
|
-
"action": "Перазагрузіць старонку"
|
|
4
|
+
"description": "Не ўдалося аднавіць злучэнне пасля некалькіх спробаў. Паспрабуйце яшчэ раз або перазагрузіце старонку, каб працягнуць.",
|
|
5
|
+
"action": "Перазагрузіць старонку",
|
|
6
|
+
"retry": "Паспрабаваць яшчэ раз",
|
|
7
|
+
"retrying": "Паўторнае падключэнне…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "Verbindung verloren",
|
|
4
|
-
"description": "Die Verbindung konnte nach mehreren Versuchen nicht wiederhergestellt werden.
|
|
5
|
-
"action": "Seite neu laden"
|
|
4
|
+
"description": "Die Verbindung konnte nach mehreren Versuchen nicht wiederhergestellt werden. Versuche es erneut oder lade die Seite neu, um fortzufahren.",
|
|
5
|
+
"action": "Seite neu laden",
|
|
6
|
+
"retry": "Erneut versuchen",
|
|
7
|
+
"retrying": "Verbindung wird wiederhergestellt…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "Connection lost",
|
|
4
|
-
"description": "We couldn't restore the connection after several attempts.
|
|
5
|
-
"action": "Reload page"
|
|
4
|
+
"description": "We couldn't restore the connection after several attempts. Try again, or reload the page to continue.",
|
|
5
|
+
"action": "Reload page",
|
|
6
|
+
"retry": "Try again",
|
|
7
|
+
"retrying": "Reconnecting…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "Conexión perdida",
|
|
4
|
-
"description": "No se pudo restablecer la conexión tras varios intentos.
|
|
5
|
-
"action": "Recargar página"
|
|
4
|
+
"description": "No se pudo restablecer la conexión tras varios intentos. Vuelve a intentarlo o recarga la página para continuar.",
|
|
5
|
+
"action": "Recargar página",
|
|
6
|
+
"retry": "Reintentar",
|
|
7
|
+
"retrying": "Reconectando…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "Connexion perdue",
|
|
4
|
-
"description": "Nous n’avons pas pu rétablir la connexion après plusieurs tentatives.
|
|
5
|
-
"action": "Actualiser la page"
|
|
4
|
+
"description": "Nous n’avons pas pu rétablir la connexion après plusieurs tentatives. Réessayez ou actualisez la page pour continuer.",
|
|
5
|
+
"action": "Actualiser la page",
|
|
6
|
+
"retry": "Réessayer",
|
|
7
|
+
"retrying": "Reconnexion…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "Utracono połączenie",
|
|
4
|
-
"description": "Nie udało się przywrócić połączenia mimo kilku prób.
|
|
5
|
-
"action": "Odśwież stronę"
|
|
4
|
+
"description": "Nie udało się przywrócić połączenia mimo kilku prób. Spróbuj ponownie lub odśwież stronę, aby kontynuować.",
|
|
5
|
+
"action": "Odśwież stronę",
|
|
6
|
+
"retry": "Spróbuj ponownie",
|
|
7
|
+
"retrying": "Ponowne łączenie…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "Соединение потеряно",
|
|
4
|
-
"description": "Не удалось восстановить соединение после нескольких попыток.
|
|
5
|
-
"action": "Перезагрузить страницу"
|
|
4
|
+
"description": "Не удалось восстановить соединение после нескольких попыток. Попробуйте снова или перезагрузите страницу, чтобы продолжить.",
|
|
5
|
+
"action": "Перезагрузить страницу",
|
|
6
|
+
"retry": "Попробовать снова",
|
|
7
|
+
"retrying": "Переподключение…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"reload": {
|
|
3
3
|
"title": "З'єднання втрачено",
|
|
4
|
-
"description": "Не вдалося відновити з'єднання після кількох спроб.
|
|
5
|
-
"action": "Перезавантажити сторінку"
|
|
4
|
+
"description": "Не вдалося відновити з'єднання після кількох спроб. Спробуйте ще раз або перезавантажте сторінку, щоб продовжити.",
|
|
5
|
+
"action": "Перезавантажити сторінку",
|
|
6
|
+
"retry": "Спробувати ще раз",
|
|
7
|
+
"retrying": "Перепідключення…"
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import type { FC } from 'react'
|
|
2
|
-
import { useSocketStatus } from '@owlmeans/client-socket'
|
|
2
|
+
import { useSocketRetry, useSocketStatus } from '@owlmeans/client-socket'
|
|
3
3
|
import { useI18nLib } from '@owlmeans/client-i18n'
|
|
4
4
|
import {
|
|
5
5
|
AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription,
|
|
6
6
|
AlertDialogFooter, AlertDialogHeader, AlertDialogTitle
|
|
7
7
|
} from '../../@/components/ui/alert-dialog.js'
|
|
8
|
+
import { Button } from '../../@/components/ui/button.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* A global, blocking "
|
|
11
|
+
* A global, blocking "connection lost" prompt — opt in per app with `cfg.socket.reloadDialog`.
|
|
11
12
|
*
|
|
12
13
|
* Mount it once, above the router (`PanelApp` does), exactly like `PanelCookieConsent`: a dialog
|
|
13
14
|
* mounted inside a route is torn down on every navigation, and this one is meant to survive them
|
|
14
15
|
* all. It opens the moment `useSocketStatus()` reports `'lost'` — every `ws()`/`useWs()`
|
|
15
|
-
* connection in the app has exhausted its own retry budget (10 minutes by default)
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* connection in the app has exhausted its own retry budget (10 minutes by default).
|
|
17
|
+
*
|
|
18
|
+
* Two exits: "Try again" revives every lost connection (`useSocketRetry()`), and the dialog stays
|
|
19
|
+
* up showing progress until they are back online — or offers both buttons again if they are lost
|
|
20
|
+
* again. "Reload page" is the fallback that always works. The status service also retries by
|
|
21
|
+
* itself when the tab or window becomes active, so a socket that died in a background tab is
|
|
22
|
+
* usually back before anyone has to press anything.
|
|
18
23
|
*
|
|
19
24
|
* Non-dismissible on two levels: Radix's `AlertDialogContent` (unlike its plain `Dialog` sibling)
|
|
20
25
|
* never treats an outside click as a request to close, so there is no `onPointerDownOutside` to
|
|
@@ -23,8 +28,11 @@ import {
|
|
|
23
28
|
*/
|
|
24
29
|
export const SocketReloadDialog: FC = () => {
|
|
25
30
|
const status = useSocketStatus()
|
|
31
|
+
const { retry, retrying } = useSocketRetry()
|
|
26
32
|
const t = useI18nLib('socket', 'reload')
|
|
27
|
-
const open = status === 'lost'
|
|
33
|
+
const open = status === 'lost' || (retrying && status === 'reconnecting')
|
|
34
|
+
// Closing still animates the content out — keep the retry in its busy state until it is gone.
|
|
35
|
+
const busy = retrying || !open
|
|
28
36
|
|
|
29
37
|
return <AlertDialog open={open}>
|
|
30
38
|
<AlertDialogContent
|
|
@@ -36,8 +44,16 @@ export const SocketReloadDialog: FC = () => {
|
|
|
36
44
|
<AlertDialogDescription>{t('description')}</AlertDialogDescription>
|
|
37
45
|
</AlertDialogHeader>
|
|
38
46
|
<AlertDialogFooter>
|
|
39
|
-
<
|
|
47
|
+
<Button variant="outline" data-socket-reload-action onClick={() => window.location.reload()}>
|
|
40
48
|
{t('action')}
|
|
49
|
+
</Button>
|
|
50
|
+
<AlertDialogAction
|
|
51
|
+
data-socket-retry-action
|
|
52
|
+
disabled={busy}
|
|
53
|
+
aria-busy={busy}
|
|
54
|
+
onClick={retry}
|
|
55
|
+
>
|
|
56
|
+
{busy ? t('retrying') : t('retry')}
|
|
41
57
|
</AlertDialogAction>
|
|
42
58
|
</AlertDialogFooter>
|
|
43
59
|
</AlertDialogContent>
|
package/src/types.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { SocketClientSettings, SocketStatusServiceAppend } from '@owlmeans/
|
|
|
7
7
|
export interface AppConfig extends Config, WithFlowConfig {
|
|
8
8
|
socket?: SocketClientSettings & {
|
|
9
9
|
/**
|
|
10
|
-
* Show `SocketReloadDialog` — a global, blocking "reload the page" prompt — once every
|
|
10
|
+
* Show `SocketReloadDialog` — a global, blocking "try again / reload the page" prompt — once every
|
|
11
11
|
* socket in the app has given up reconnecting (`useSocketStatus() === 'lost'`). Off by
|
|
12
12
|
* default: an app opts in once it has decided that outcome deserves a full-page prompt
|
|
13
13
|
* rather than the caller's own fallback UI.
|
package/tests/harness/mount.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import '../../src/@/globals.css'
|
|
2
2
|
|
|
3
3
|
import type { FC, PropsWithChildren } from 'react'
|
|
4
|
+
import { useState } from 'react'
|
|
4
5
|
import { createRoot } from 'react-dom/client'
|
|
5
6
|
import { config } from '@owlmeans/client-context'
|
|
6
7
|
import { AppType, service } from '@owlmeans/config'
|
|
@@ -184,12 +185,23 @@ const ReportsGroup: FC<PropsWithChildren> = ({ children }) => <div id="reports-g
|
|
|
184
185
|
*/
|
|
185
186
|
const SocketStatusScreen: FC = () => {
|
|
186
187
|
const context = useContext()
|
|
188
|
+
// A revive the way a real carrier's goes: the connection is retrying again the moment it is
|
|
189
|
+
// asked to. How that retry ends is up to the test — `#report-online` or `#report-lost`.
|
|
190
|
+
const [revived, setRevived] = useState(0)
|
|
191
|
+
const reportLost = () => context.socketStatus().report(HARNESS_SOCKET_ID, 'lost', () => {
|
|
192
|
+
setRevived(n => n + 1)
|
|
193
|
+
context.socketStatus().report(HARNESS_SOCKET_ID, 'reconnecting')
|
|
194
|
+
})
|
|
187
195
|
|
|
188
196
|
return <div id="socket-status">
|
|
189
197
|
socket-status-screen
|
|
190
|
-
<
|
|
198
|
+
<span id="revived">{revived}</span>
|
|
199
|
+
<button id="report-lost" onClick={reportLost}>
|
|
191
200
|
lose connection
|
|
192
201
|
</button>
|
|
202
|
+
<button id="report-online" onClick={() => context.socketStatus().report(HARNESS_SOCKET_ID, 'online')}>
|
|
203
|
+
reconnected
|
|
204
|
+
</button>
|
|
193
205
|
<button id="release-lost" onClick={() => context.socketStatus().release(HARNESS_SOCKET_ID)}>
|
|
194
206
|
restore connection
|
|
195
207
|
</button>
|
|
@@ -11,6 +11,19 @@ afterAll(async () => {
|
|
|
11
11
|
|
|
12
12
|
const open = async (path: string) => mountComponent({ url: `${HARNESS_URL.replace(/\/$/, '')}${path}` })
|
|
13
13
|
|
|
14
|
+
type Page = Awaited<ReturnType<typeof open>>['page']
|
|
15
|
+
|
|
16
|
+
// The modal overlay covers the harness buttons and would take a pointer click itself.
|
|
17
|
+
const clickBehindDialog = async (page: Page, selector: string) =>
|
|
18
|
+
page.evaluate(sel => document.querySelector<HTMLButtonElement>(sel)?.click(), selector)
|
|
19
|
+
|
|
20
|
+
const revivedCount = async (page: Page, count: number) =>
|
|
21
|
+
page.waitForFunction(n => document.querySelector('#revived')?.textContent === String(n), count)
|
|
22
|
+
|
|
23
|
+
const retryDisabled = async (page: Page, disabled: boolean) => page.waitForFunction(
|
|
24
|
+
d => document.querySelector<HTMLButtonElement>('[data-socket-retry-action]')?.disabled === d, disabled
|
|
25
|
+
)
|
|
26
|
+
|
|
14
27
|
describe('@owlmeans/web-panel — the socket reload dialog', () => {
|
|
15
28
|
test('stays unmounted until the connection is reported lost', async () => {
|
|
16
29
|
const { page, close } = await open('/socket')
|
|
@@ -38,6 +51,13 @@ describe('@owlmeans/web-panel — the socket reload dialog', () => {
|
|
|
38
51
|
await page.mouse.click(2, 2)
|
|
39
52
|
expect(await dialog.isVisible()).toBe(true)
|
|
40
53
|
|
|
54
|
+
// The secondary button's label must be readable, not the primary's text on an outline fill.
|
|
55
|
+
const reloadColors = await page.locator('[data-socket-reload-action]').evaluate(el => {
|
|
56
|
+
const style = getComputedStyle(el)
|
|
57
|
+
return { color: style.color, background: style.backgroundColor }
|
|
58
|
+
})
|
|
59
|
+
expect(reloadColors.color).not.toBe(reloadColors.background)
|
|
60
|
+
|
|
41
61
|
const [navigated] = await Promise.all([
|
|
42
62
|
page.waitForEvent('load'),
|
|
43
63
|
page.locator('[data-socket-reload-action]').click(),
|
|
@@ -53,6 +73,71 @@ describe('@owlmeans/web-panel — the socket reload dialog', () => {
|
|
|
53
73
|
}
|
|
54
74
|
}, TIMEOUT)
|
|
55
75
|
|
|
76
|
+
test('Try again revives the lost connection, shows progress, and closes once it is back', async () => {
|
|
77
|
+
const { page, close } = await open('/socket')
|
|
78
|
+
try {
|
|
79
|
+
await page.waitForSelector('#socket-status')
|
|
80
|
+
await page.locator('#report-lost').click()
|
|
81
|
+
|
|
82
|
+
const dialog = page.locator('[data-socket-reload-dialog]')
|
|
83
|
+
const retry = page.locator('[data-socket-retry-action]')
|
|
84
|
+
await dialog.waitFor({ state: 'visible' })
|
|
85
|
+
expect(await retry.isEnabled()).toBe(true)
|
|
86
|
+
|
|
87
|
+
await retry.click()
|
|
88
|
+
await revivedCount(page, 1)
|
|
89
|
+
await retryDisabled(page, true)
|
|
90
|
+
// Still up while the revived connection is retrying — closing now would read as "fixed".
|
|
91
|
+
expect(await dialog.isVisible()).toBe(true)
|
|
92
|
+
expect(await retry.getAttribute('aria-busy')).toBe('true')
|
|
93
|
+
|
|
94
|
+
await clickBehindDialog(page, '#report-online')
|
|
95
|
+
await dialog.waitFor({ state: 'detached' })
|
|
96
|
+
} finally {
|
|
97
|
+
await close()
|
|
98
|
+
}
|
|
99
|
+
}, TIMEOUT)
|
|
100
|
+
|
|
101
|
+
test('a retry that fails again offers both buttons again', async () => {
|
|
102
|
+
const { page, close } = await open('/socket')
|
|
103
|
+
try {
|
|
104
|
+
await page.waitForSelector('#socket-status')
|
|
105
|
+
await page.locator('#report-lost').click()
|
|
106
|
+
const retry = page.locator('[data-socket-retry-action]')
|
|
107
|
+
await retry.click()
|
|
108
|
+
await retryDisabled(page, true)
|
|
109
|
+
|
|
110
|
+
await clickBehindDialog(page, '#report-lost')
|
|
111
|
+
await retryDisabled(page, false)
|
|
112
|
+
expect(await page.locator('[data-socket-reload-dialog]').isVisible()).toBe(true)
|
|
113
|
+
} finally {
|
|
114
|
+
await close()
|
|
115
|
+
}
|
|
116
|
+
}, TIMEOUT)
|
|
117
|
+
|
|
118
|
+
test('retries on its own when the tab or window becomes active again', async () => {
|
|
119
|
+
const { page, close } = await open('/socket')
|
|
120
|
+
try {
|
|
121
|
+
await page.waitForSelector('#socket-status')
|
|
122
|
+
await page.locator('#report-lost').click()
|
|
123
|
+
await page.locator('[data-socket-reload-dialog]').waitFor({ state: 'visible' })
|
|
124
|
+
|
|
125
|
+
await page.evaluate(() => document.dispatchEvent(new Event('visibilitychange')))
|
|
126
|
+
await revivedCount(page, 1)
|
|
127
|
+
|
|
128
|
+
// Nothing is lost any more, so a second activation has nothing to revive.
|
|
129
|
+
await page.evaluate(() => window.dispatchEvent(new Event('focus')))
|
|
130
|
+
await page.waitForTimeout(100)
|
|
131
|
+
expect(await page.locator('#revived').textContent()).toBe('1')
|
|
132
|
+
|
|
133
|
+
await clickBehindDialog(page, '#report-lost')
|
|
134
|
+
await page.evaluate(() => window.dispatchEvent(new Event('focus')))
|
|
135
|
+
await revivedCount(page, 2)
|
|
136
|
+
} finally {
|
|
137
|
+
await close()
|
|
138
|
+
}
|
|
139
|
+
}, TIMEOUT)
|
|
140
|
+
|
|
56
141
|
test('never mounts when the app has not opted in via cfg.socket.reloadDialog', async () => {
|
|
57
142
|
const { page, close } = await open('/socket?reloadDialog=0')
|
|
58
143
|
try {
|