dsh-offwork 1.1.0 → 1.1.2

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +50 -32
  3. package/assets/animations/NOTICE.md +15 -0
  4. package/assets/animations/alpha-probe.webm +0 -0
  5. package/assets/animations/ornate-drink.webm +0 -0
  6. package/assets/animations/ornate-greet.webm +0 -0
  7. package/assets/animations/ornate-idle.webm +0 -0
  8. package/assets/animations/whale-drink.webm +0 -0
  9. package/assets/animations/whale-greet.webm +0 -0
  10. package/assets/characters/NOTICE.md +4 -0
  11. package/assets/characters/whale-actions.webp +0 -0
  12. package/assets/characters/whale-ornate-actions.webp +0 -0
  13. package/client.js +2970 -770
  14. package/client.source.js +1668 -444
  15. package/companion.js +6 -9
  16. package/countdown.js +83 -0
  17. package/hydration-store.js +55 -16
  18. package/hydration.js +231 -250
  19. package/index.js +2 -0
  20. package/locale/en.json +1 -1
  21. package/locale/zh.json +1 -1
  22. package/media-manifest.js +29 -0
  23. package/media-server.js +123 -0
  24. package/package.json +9 -2
  25. package/pet-interactions.js +78 -7
  26. package/pet-motion.js +111 -0
  27. package/pet-reminder.js +20 -0
  28. package/pet-surfaces.js +130 -6
  29. package/pet-video.js +380 -0
  30. package/pet-view.js +129 -13
  31. package/tests/classic-overview.test.mjs +77 -0
  32. package/tests/companion.test.mjs +12 -12
  33. package/tests/countdown.test.mjs +115 -0
  34. package/tests/host.test.mjs +6 -0
  35. package/tests/hydration-extras.test.mjs +168 -204
  36. package/tests/hydration-store.test.mjs +199 -213
  37. package/tests/hydration.test.mjs +116 -186
  38. package/tests/media-server.test.mjs +179 -0
  39. package/tests/pet-character.test.mjs +8 -2
  40. package/tests/pet-motion.test.mjs +139 -0
  41. package/tests/pet-reminder.test.mjs +71 -0
  42. package/tests/pet-video.test.mjs +503 -0
  43. package/tools/build.mjs +62 -13
  44. package/tools/check-package.mjs +19 -0
  45. package/ui-utils.js +51 -1
@@ -0,0 +1,139 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createPetMotion } from '../pet-motion.js';
4
+ import { petArtwork } from '../pet-view.js';
5
+
6
+ function fixture(options) {
7
+ const document = Object.assign(new EventTarget(), { hidden: false });
8
+ const media = Object.assign(new EventTarget(), { matches: false });
9
+ const runs = [];
10
+ const element = {
11
+ dataset: {},
12
+ style: {},
13
+ animate(frames, timing) {
14
+ let finish, reject;
15
+ const finished = new Promise((resolve, fail) => {
16
+ finish = resolve;
17
+ reject = fail;
18
+ });
19
+ const run = {
20
+ frames,
21
+ timing,
22
+ finished,
23
+ finish,
24
+ cancelled: false,
25
+ cancel() {
26
+ this.cancelled = true;
27
+ reject(new Error('cancelled'));
28
+ },
29
+ };
30
+ runs.push(run);
31
+ return run;
32
+ },
33
+ };
34
+ const controller = createPetMotion(element, { document, matchMedia: () => media }, options);
35
+ return { document, media, runs, element, controller };
36
+ }
37
+
38
+ test('explicit reactions interrupt safely and old completion cannot clear the next action', async () => {
39
+ const { controller: c, element: e, runs } = fixture();
40
+ assert.equal(e.dataset.motion, 'idle');
41
+ assert.equal(c.play('look'), true);
42
+ assert.equal(c.play('greet'), true);
43
+ await Promise.resolve();
44
+ assert.equal(runs[0].cancelled, true);
45
+ assert.equal(e.dataset.action, 'greet');
46
+ c.play('celebrate');
47
+ assert.equal(c.play('look'), false);
48
+ assert.equal(c.play('greet'), false);
49
+ await Promise.resolve();
50
+ assert.equal(e.dataset.action, 'celebrate');
51
+ runs.at(-1).finish();
52
+ await Promise.resolve();
53
+ assert.equal(e.dataset.action, undefined);
54
+ assert.equal(e.dataset.motion, 'idle');
55
+ c.destroy();
56
+ });
57
+
58
+ test('motion preference, visibility and live reduced-motion changes stop actions without replay', async () => {
59
+ const { controller: c, element: e, document: d, media, runs } = fixture();
60
+ c.play('celebrate');
61
+ d.hidden = true;
62
+ d.dispatchEvent(new Event('visibilitychange'));
63
+ assert.equal(e.dataset.motion, 'off');
64
+ assert.equal(e.dataset.action, undefined);
65
+ assert.equal(runs[0].cancelled, true);
66
+ d.hidden = false;
67
+ d.dispatchEvent(new Event('visibilitychange'));
68
+ assert.equal(e.dataset.motion, 'idle');
69
+ assert.equal(runs.length, 1);
70
+ media.matches = true;
71
+ media.dispatchEvent(new Event('change'));
72
+ assert.equal(c.play('greet'), false);
73
+ assert.equal(e.dataset.motion, 'off');
74
+ media.matches = false;
75
+ media.dispatchEvent(new Event('change'));
76
+ c.configure({ enabled: false, resting: false });
77
+ assert.equal(c.play('celebrate'), false);
78
+ c.configure({ enabled: true, resting: false });
79
+ c.play('celebrate');
80
+ c.configure({ enabled: true, resting: true });
81
+ assert.equal(e.dataset.action, undefined);
82
+ assert.equal(e.dataset.motion, 'off');
83
+ assert.equal(c.play('greet'), true);
84
+ c.destroy();
85
+ d.dispatchEvent(new Event('visibilitychange'));
86
+ assert.equal(e.dataset.motion, 'off');
87
+ assert.equal(c.play('greet'), false);
88
+ await Promise.resolve();
89
+ });
90
+
91
+ test('drag cancels reactions, release settles once, cancellation stays still', async () => {
92
+ const { controller: c, element: e, runs } = fixture();
93
+ c.play('greet');
94
+ c.drag(-20);
95
+ assert.equal(runs[0].cancelled, true);
96
+ assert.equal(e.dataset.action, 'drag');
97
+ assert.match(e.style.transform, /rotate\(-2deg\)/);
98
+ assert.equal(c.play('look'), false);
99
+ c.release();
100
+ assert.equal(e.style.transform, '');
101
+ assert.equal(e.dataset.action, 'settle');
102
+ c.drag(20);
103
+ c.release(true);
104
+ assert.equal(e.dataset.action, undefined);
105
+ assert.equal(runs.length, 2);
106
+ c.destroy();
107
+ await Promise.resolve();
108
+ });
109
+
110
+ test('missing or failed animation API retains usable static artwork', () => {
111
+ const { controller: c, element: e } = fixture();
112
+ e.animate = undefined;
113
+ assert.equal(c.play('greet'), false);
114
+ e.animate = () => {
115
+ throw new Error('unavailable');
116
+ };
117
+ assert.equal(c.play('greet'), false);
118
+ assert.equal(e.dataset.action, undefined);
119
+ assert.equal(c.play('__proto__'), false);
120
+ c.destroy();
121
+ });
122
+
123
+ test('interactive artwork keeps its DOM identity across clock poses and drinking feedback', () => {
124
+ const h = (type, props) => ({ type, props });
125
+ for (const model of ['whale-dress', 'xiaoxie-original']) {
126
+ const render = (pose, token) =>
127
+ petArtwork(
128
+ h,
129
+ { pose, animate: true },
130
+ 132,
131
+ token,
132
+ model,
133
+ { 'whale-dress': 'data:image/webp;base64,test' },
134
+ true,
135
+ );
136
+ assert.equal(render('calm', '').props.key, render('happy', 'drink-1').props.key);
137
+ assert.equal(render('calm', '').props['data-interactive'], true);
138
+ }
139
+ });
@@ -0,0 +1,71 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { initialPetReminder, nextPetReminder, dismissPetReminder } from '../pet-reminder.js';
4
+ const input = (patch = {}) => ({
5
+ ready: true,
6
+ eventId: 'shift:1',
7
+ phase: 'countdown',
8
+ visible: true,
9
+ available: true,
10
+ ownsPrompt: false,
11
+ ...patch,
12
+ });
13
+
14
+ test('loading or refreshing an already-due reminder never opens a card', () => {
15
+ let state = nextPetReminder(initialPetReminder, input({ ready: false, eventId: null }));
16
+ state = nextPetReminder(state, input({ phase: 'due', ownsPrompt: true }));
17
+ assert.equal(state.open, false);
18
+ assert.equal(state.armed, false);
19
+ assert.equal(nextPetReminder(state, input({ phase: 'due', ownsPrompt: true })), state);
20
+ const explicitlyOpened = { ...state, open: true };
21
+ assert.equal(
22
+ nextPetReminder(explicitlyOpened, input({ phase: 'due', ownsPrompt: true })).open,
23
+ true,
24
+ );
25
+ });
26
+
27
+ test('a live countdown opens only for its lease owner and stays dismissed for the same due phase', () => {
28
+ let state = nextPetReminder(initialPetReminder, input());
29
+ assert.equal(state.armed, true);
30
+ state = nextPetReminder(state, input({ phase: 'due' }));
31
+ assert.equal(state.open, false);
32
+ assert.equal(state.armed, true);
33
+ state = nextPetReminder(state, input({ phase: 'due', ownsPrompt: true }));
34
+ assert.equal(state.open, true);
35
+ assert.equal(nextPetReminder(state, input({ phase: 'due', ownsPrompt: true })), state);
36
+ state = dismissPetReminder(state);
37
+ assert.equal(nextPetReminder(state, input({ phase: 'due', ownsPrompt: true })).open, false);
38
+ state = nextPetReminder(state, input({ eventId: 'shift:2' }));
39
+ assert.equal(
40
+ nextPetReminder(state, input({ eventId: 'shift:2', phase: 'due', ownsPrompt: true })).open,
41
+ true,
42
+ );
43
+ });
44
+
45
+ test('open cards, background state and lease loss stop popups without delayed replay', () => {
46
+ for (const patch of [{ available: false }, { visible: false }, { ready: false }]) {
47
+ let state = nextPetReminder(initialPetReminder, input());
48
+ state = nextPetReminder(state, input({ phase: 'due', ownsPrompt: true, ...patch }));
49
+ assert.equal(state.open, false);
50
+ assert.equal(nextPetReminder(state, input({ phase: 'due', ownsPrompt: true })).open, false);
51
+ }
52
+ let state = nextPetReminder(initialPetReminder, input());
53
+ state = nextPetReminder(state, input({ phase: 'due', ownsPrompt: true }));
54
+ state = nextPetReminder(state, input({ phase: 'due', ownsPrompt: false }));
55
+ assert.equal(state.open, false);
56
+ assert.equal(nextPetReminder(state, input({ phase: 'due', ownsPrompt: true })).open, false);
57
+ });
58
+
59
+ test('quiet or unpaid-break phases can arm a reminder for work resumption without changing business state', () => {
60
+ for (const phase of ['quiet', 'break']) {
61
+ const previous = Object.freeze({ eventId: 'shift:1', armed: false, open: false });
62
+ const state = nextPetReminder(previous, input({ phase }));
63
+ assert.equal(state.armed, true);
64
+ assert.equal(nextPetReminder(state, input({ phase: 'due', ownsPrompt: true })).open, true);
65
+ assert.deepEqual(previous, { eventId: 'shift:1', armed: false, open: false });
66
+ }
67
+ for (const phase of ['off', 'waiting', 'paused', 'finished']) {
68
+ const state = nextPetReminder(nextPetReminder(initialPetReminder, input()), input({ phase }));
69
+ assert.equal(nextPetReminder(state, input({ phase: 'due', ownsPrompt: true })).open, false);
70
+ }
71
+ });