@freqhole/playlistz 0.0.1
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/.changeset/config.json +11 -0
- package/.changeset/nice-wolves-thank.md +5 -0
- package/.freqhole-versions.json +4 -0
- package/.github/copilot-instructions.md +201 -0
- package/.github/workflows/changesets.yml +50 -0
- package/.github/workflows/npm-publish.yml +124 -0
- package/.github/workflows/pr-checks.yml +103 -0
- package/README.md +30 -0
- package/build-component.js +141 -0
- package/build-zip-bundle-lib.js +44 -0
- package/config/playwright.config.ts +47 -0
- package/config/vite.config.ts +44 -0
- package/config/vitest.config.ts +39 -0
- package/dist/assets/automerge_wasm_bg-Cik4BF9l.wasm +0 -0
- package/dist/assets/index-CbOXzGiA.js +216 -0
- package/dist/assets/index-CbOXzGiA.js.map +1 -0
- package/dist/assets/index-TvJ6RFpy.css +1 -0
- package/dist/assets/midden-DceCrT_L.js +2 -0
- package/dist/assets/midden-DceCrT_L.js.map +1 -0
- package/dist/assets/midden_bg-BLhfGIU-.wasm +0 -0
- package/dist/index.html +55 -0
- package/dist/sw.js +134 -0
- package/docs/AUTOMERGE_P2P_PLAN.md +233 -0
- package/docs/COLLABORATIVE_SHARING_PLAN.md +188 -0
- package/docs/E2E_TESTID_PLAN.md +234 -0
- package/docs/IROH_P2P_PLAN.md +302 -0
- package/docs/ROADMAP.md +695 -0
- package/docs/TODO.md +167 -0
- package/docs/bundle-embedding-plan.md +134 -0
- package/docs/standalone-refactor.md +184 -0
- package/e2e/all-playlists.spec.ts +220 -0
- package/e2e/audio-player.spec.ts +226 -0
- package/e2e/collaborative-features.spec.ts +229 -0
- package/e2e/contexts.ts +238 -0
- package/e2e/edit-panel.spec.ts +87 -0
- package/e2e/fixtures/bare-glitch-1s.m4a +0 -0
- package/e2e/fixtures/bare-glitch-1s.mp3 +0 -0
- package/e2e/fixtures/bare-glitch-1s.ogg +0 -0
- package/e2e/fixtures/chord-stack-3s.wav +0 -0
- package/e2e/fixtures/cover-anim.gif +0 -0
- package/e2e/fixtures/cover-blue.png +0 -0
- package/e2e/fixtures/cover-checkers.png +0 -0
- package/e2e/fixtures/cover-gradient.jpg +0 -0
- package/e2e/fixtures/cover-mono.gif +0 -0
- package/e2e/fixtures/cover-noise.png +0 -0
- package/e2e/fixtures/cover-plasma.webp +0 -0
- package/e2e/fixtures/cover-portrait.jpg +0 -0
- package/e2e/fixtures/cover-red.png +0 -0
- package/e2e/fixtures/cover-thumb.jpg +0 -0
- package/e2e/fixtures/cover-wide.webp +0 -0
- package/e2e/fixtures/generate.mjs +257 -0
- package/e2e/fixtures/long-drone-90s.mp3 +0 -0
- package/e2e/fixtures/noisy-binaural-8s.mp3 +0 -0
- package/e2e/fixtures/tagged-a3-4s.m4a +0 -0
- package/e2e/fixtures/tagged-a3-4s.mp3 +0 -0
- package/e2e/fixtures/tagged-a3-4s.ogg +0 -0
- package/e2e/fixtures/tagged-c5-3s.m4a +0 -0
- package/e2e/fixtures/tagged-c5-3s.mp3 +0 -0
- package/e2e/fixtures/tagged-c5-3s.ogg +0 -0
- package/e2e/fixtures/tagged-f4-6s.m4a +0 -0
- package/e2e/fixtures/tagged-f4-6s.mp3 +0 -0
- package/e2e/fixtures/tagged-f4-6s.ogg +0 -0
- package/e2e/fixtures/tone-220hz-10s.wav +0 -0
- package/e2e/fixtures/tone-440hz-2s.wav +0 -0
- package/e2e/fixtures/tone-880hz-5s.wav +0 -0
- package/e2e/fixtures/tone-stereo-3s.wav +0 -0
- package/e2e/fixtures/user-provided/README.md +1 -0
- package/e2e/helpers/app.ts +143 -0
- package/e2e/helpers/hooks.ts +133 -0
- package/e2e/helpers/index.ts +12 -0
- package/e2e/helpers/media.ts +125 -0
- package/e2e/helpers.ts +10 -0
- package/e2e/p2p-collaboration.spec.ts +356 -0
- package/e2e/p2p-multi-peer.spec.ts +723 -0
- package/e2e/p2p-states.spec.ts +302 -0
- package/e2e/playback.spec.ts +56 -0
- package/e2e/playlist-crud.spec.ts +126 -0
- package/e2e/share-link-autoplay.spec.ts +129 -0
- package/e2e/sharing-access.spec.ts +205 -0
- package/e2e/sharing.spec.ts +195 -0
- package/e2e/song-cache-state.spec.ts +202 -0
- package/e2e/zip-bundle.spec.ts +855 -0
- package/eslint.config.js +114 -0
- package/index.html +54 -0
- package/package.json +119 -0
- package/public/sw.js +134 -0
- package/scripts/use-local.mjs +37 -0
- package/scripts/use-published.mjs +37 -0
- package/src/App.tsx +9 -0
- package/src/cli/check.ts +164 -0
- package/src/cli/generate.ts +184 -0
- package/src/cli/http.ts +88 -0
- package/src/cli/index.ts +65 -0
- package/src/cli/init.ts +18 -0
- package/src/components/AllPlaylistsPanel.tsx +713 -0
- package/src/components/AudioPlayer.tsx +122 -0
- package/src/components/MarqueeText.tsx +101 -0
- package/src/components/PlaylistCoverModal.tsx +519 -0
- package/src/components/PlaylistEditPanel.tsx +803 -0
- package/src/components/PlaylistSharePanel.tsx +1020 -0
- package/src/components/ShareLinkKnockPanel.tsx +144 -0
- package/src/components/SharePanel.tsx +584 -0
- package/src/components/SongEditModal.tsx +453 -0
- package/src/components/SongEditPanel.tsx +578 -0
- package/src/components/SongRow.tsx +689 -0
- package/src/components/index.tsx +494 -0
- package/src/components/playlist/index.tsx +1203 -0
- package/src/context/PlaylistzContext.tsx +74 -0
- package/src/dev-hooks.ts +35 -0
- package/src/hooks/createDocIndexQuery.ts +53 -0
- package/src/hooks/createDocStore.test.ts +303 -0
- package/src/hooks/createDocStore.ts +90 -0
- package/src/hooks/useDragAndDrop.test.ts +474 -0
- package/src/hooks/useDragAndDrop.ts +400 -0
- package/src/hooks/useImageModal.test.ts +174 -0
- package/src/hooks/useImageModal.ts +201 -0
- package/src/hooks/usePlaylistManager.test.ts +453 -0
- package/src/hooks/usePlaylistManager.ts +685 -0
- package/src/hooks/usePlaylistsQuery.test.tsx +120 -0
- package/src/hooks/usePlaylistsQuery.ts +44 -0
- package/src/hooks/useSongState.test.ts +236 -0
- package/src/hooks/useSongState.ts +114 -0
- package/src/hooks/useUIState.ts +71 -0
- package/src/index.tsx +18 -0
- package/src/services/audioService.dev.ts +22 -0
- package/src/services/audioService.test.ts +1226 -0
- package/src/services/audioService.ts +1395 -0
- package/src/services/automergeRepo.test.ts +269 -0
- package/src/services/automergeRepo.ts +226 -0
- package/src/services/blobTransferService.dev.ts +119 -0
- package/src/services/blobTransferService.test.ts +441 -0
- package/src/services/blobTransferService.ts +702 -0
- package/src/services/docIndexService.test.ts +179 -0
- package/src/services/docIndexService.ts +118 -0
- package/src/services/fileProcessingService.test.ts +554 -0
- package/src/services/fileProcessingService.ts +239 -0
- package/src/services/imageService.test.ts +701 -0
- package/src/services/imageService.ts +365 -0
- package/src/services/indexedDBService.integration.test.ts +104 -0
- package/src/services/indexedDBService.test.ts +202 -0
- package/src/services/indexedDBService.ts +436 -0
- package/src/services/offlineService.test.ts +661 -0
- package/src/services/offlineService.ts +382 -0
- package/src/services/p2pService.test.ts +305 -0
- package/src/services/p2pService.ts +344 -0
- package/src/services/playlistDocService.test.ts +448 -0
- package/src/services/playlistDocService.ts +707 -0
- package/src/services/playlistDownloadService.test.ts +674 -0
- package/src/services/playlistDownloadService.ts +389 -0
- package/src/services/sharingService.test.ts +812 -0
- package/src/services/sharingService.ts +1073 -0
- package/src/services/sharingState.ts +161 -0
- package/src/services/songReactivity.test.ts +620 -0
- package/src/services/songReactivity.ts +145 -0
- package/src/services/standaloneService.test.ts +1025 -0
- package/src/services/standaloneService.ts +588 -0
- package/src/services/streamingAudioService.test.ts +275 -0
- package/src/services/streamingAudioService.ts +166 -0
- package/src/styles.css +428 -0
- package/src/test-setup.ts +547 -0
- package/src/types/global.d.ts +40 -0
- package/src/types/playlist.ts +99 -0
- package/src/utils/hashUtils.ts +41 -0
- package/src/utils/log.ts +97 -0
- package/src/utils/m3u.test.ts +172 -0
- package/src/utils/m3u.ts +136 -0
- package/src/utils/mockData.ts +166 -0
- package/src/utils/standaloneTemplates.test.ts +175 -0
- package/src/utils/standaloneTemplates.ts +83 -0
- package/src/utils/swTemplate.ts +84 -0
- package/src/utils/timeUtils.ts +166 -0
- package/src/utils/typeGuards.ts +171 -0
- package/src/web-component.tsx +98 -0
- package/src/zip-bundle/index.ts +7 -0
- package/src/zip-bundle/m3u.ts +45 -0
- package/src/zip-bundle/types.ts +50 -0
- package/src/zip-bundle/utils.ts +33 -0
- package/src/zip-bundle/zipBuilder.ts +309 -0
- package/tailwind.config.js +55 -0
- package/tsconfig.json +43 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// e2e: two-peer collaboration access request and collaborative-mode tests.
|
|
2
|
+
//
|
|
3
|
+
// tests here require real iroh relay connections and are tagged @p2p.
|
|
4
|
+
// run with: npm run test:e2e:p2p
|
|
5
|
+
//
|
|
6
|
+
// scenarios covered:
|
|
7
|
+
// - peer B requests edit access → knock appears in peer A's inbox → A accepts → B sees granted
|
|
8
|
+
// - peer A enables collaborative mode → peer B's request is auto-accepted
|
|
9
|
+
// - peer A denies a collaboration request → peer B sees denied status
|
|
10
|
+
|
|
11
|
+
import { test, expect } from "@playwright/test";
|
|
12
|
+
import {
|
|
13
|
+
resetAppState,
|
|
14
|
+
createPlaylistViaUI,
|
|
15
|
+
logTs,
|
|
16
|
+
} from "./helpers.js";
|
|
17
|
+
|
|
18
|
+
// helper: enable p2p for a page, return the share link
|
|
19
|
+
async function enableP2PAndGetShareLink(
|
|
20
|
+
page: import("@playwright/test").Page,
|
|
21
|
+
tag: string
|
|
22
|
+
): Promise<string> {
|
|
23
|
+
await page.getByTestId("btn-share-playlist").click();
|
|
24
|
+
logTs(`[e2e] ${tag}: enabling p2p...`);
|
|
25
|
+
await page.getByTestId("btn-enable-sharing").click();
|
|
26
|
+
await expect(page.getByTestId("sharing-status")).toBeVisible({
|
|
27
|
+
timeout: 180_000,
|
|
28
|
+
});
|
|
29
|
+
logTs(`[e2e] ${tag}: p2p node online`);
|
|
30
|
+
const link = await page.locator("input[readonly]").first().inputValue();
|
|
31
|
+
expect(link).toContain("#share/");
|
|
32
|
+
return link;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// helper: peer B subscribes to A's share link (public mode - direct sync)
|
|
36
|
+
async function subscribeToPublicPlaylist(
|
|
37
|
+
page: import("@playwright/test").Page,
|
|
38
|
+
shareUrl: string,
|
|
39
|
+
tag: string,
|
|
40
|
+
expectedTitle: string
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
// close share panel if open
|
|
43
|
+
if (
|
|
44
|
+
await page
|
|
45
|
+
.getByTestId("share-panel")
|
|
46
|
+
.isVisible({ timeout: 500 })
|
|
47
|
+
.catch(() => false)
|
|
48
|
+
) {
|
|
49
|
+
await page.getByTestId("btn-share-playlist").click();
|
|
50
|
+
}
|
|
51
|
+
await page.getByTestId("btn-all-playlists").click();
|
|
52
|
+
await page.getByTestId("all-playlists-panel").waitFor({ timeout: 5000 });
|
|
53
|
+
await page.getByTestId("input-search-playlists").fill(shareUrl);
|
|
54
|
+
logTs(`[e2e] ${tag}: opening share link...`);
|
|
55
|
+
await expect(page.getByTestId("all-playlists-panel")).not.toBeVisible({
|
|
56
|
+
timeout: 120_000,
|
|
57
|
+
});
|
|
58
|
+
logTs(`[e2e] ${tag}: subscribed`);
|
|
59
|
+
await expect(page.getByTestId("input-playlist-title")).toHaveValue(
|
|
60
|
+
expectedTitle,
|
|
61
|
+
{ timeout: 30_000 }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// -----------------------------------------------------------------------
|
|
66
|
+
// collaboration request - explicit accept
|
|
67
|
+
// -----------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
test(
|
|
70
|
+
"peer B requests collab access, A accepts, B sees access granted @p2p",
|
|
71
|
+
async ({ browser }) => {
|
|
72
|
+
test.setTimeout(600_000);
|
|
73
|
+
|
|
74
|
+
const ctxA = await browser.newContext();
|
|
75
|
+
const ctxB = await browser.newContext();
|
|
76
|
+
const pageA = await ctxA.newPage();
|
|
77
|
+
const pageB = await ctxB.newPage();
|
|
78
|
+
|
|
79
|
+
const fwd =
|
|
80
|
+
(tag: string) => (msg: import("@playwright/test").ConsoleMessage) => {
|
|
81
|
+
logTs(`[${tag}] ${msg.text()}`);
|
|
82
|
+
};
|
|
83
|
+
pageA.on("console", fwd("peerA"));
|
|
84
|
+
pageB.on("console", fwd("peerB"));
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// --- peer A: create playlist in public mode ---
|
|
88
|
+
await resetAppState(pageA);
|
|
89
|
+
await createPlaylistViaUI(pageA);
|
|
90
|
+
await pageA
|
|
91
|
+
.getByTestId("input-playlist-title")
|
|
92
|
+
.fill("collab-test-accept");
|
|
93
|
+
await pageA.getByTestId("input-playlist-title").blur();
|
|
94
|
+
await pageA.waitForTimeout(300);
|
|
95
|
+
|
|
96
|
+
const shareUrl = await enableP2PAndGetShareLink(pageA, "peerA");
|
|
97
|
+
|
|
98
|
+
// set mode to public so peer B can subscribe without knocking
|
|
99
|
+
await pageA.getByTestId("btn-mode-public").click();
|
|
100
|
+
await expect(pageA.getByTestId("btn-mode-public")).toHaveAttribute(
|
|
101
|
+
"aria-pressed",
|
|
102
|
+
"true"
|
|
103
|
+
);
|
|
104
|
+
logTs("[e2e] peerA: mode set to public");
|
|
105
|
+
|
|
106
|
+
// close share panel
|
|
107
|
+
await pageA.getByTestId("btn-share-playlist").click();
|
|
108
|
+
|
|
109
|
+
// --- peer B: boot p2p and subscribe to A's playlist ---
|
|
110
|
+
await resetAppState(pageB);
|
|
111
|
+
await createPlaylistViaUI(pageB);
|
|
112
|
+
await enableP2PAndGetShareLink(pageB, "peerB");
|
|
113
|
+
// close B's own share panel before subscribing
|
|
114
|
+
await pageB.getByTestId("btn-share-playlist").click();
|
|
115
|
+
|
|
116
|
+
await subscribeToPublicPlaylist(
|
|
117
|
+
pageB,
|
|
118
|
+
shareUrl,
|
|
119
|
+
"peerB",
|
|
120
|
+
"collab-test-accept"
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// B should see the subscribed banner (read only)
|
|
124
|
+
await expect(pageB.getByTestId("subscribed-banner")).toBeVisible();
|
|
125
|
+
logTs("[e2e] peerB: subscribed, read-only banner visible");
|
|
126
|
+
|
|
127
|
+
// --- peer B: request collaboration access ---
|
|
128
|
+
await pageB.getByTestId("btn-share-playlist").click();
|
|
129
|
+
await pageB.getByTestId("share-panel").waitFor({ timeout: 5000 });
|
|
130
|
+
await expect(
|
|
131
|
+
pageB.getByTestId("btn-request-collab-access")
|
|
132
|
+
).toBeVisible();
|
|
133
|
+
|
|
134
|
+
await pageB
|
|
135
|
+
.getByTestId("input-collab-request-message")
|
|
136
|
+
.fill("hey, can i edit this?");
|
|
137
|
+
await pageB.getByTestId("btn-request-collab-access").click();
|
|
138
|
+
logTs("[e2e] peerB: sent collaboration request");
|
|
139
|
+
|
|
140
|
+
// B should see pending status
|
|
141
|
+
await expect(pageB.getByTestId("collab-request-status")).toContainText(
|
|
142
|
+
"waiting for owner approval",
|
|
143
|
+
{ timeout: 30_000 }
|
|
144
|
+
);
|
|
145
|
+
logTs("[e2e] peerB: status shows waiting for approval");
|
|
146
|
+
|
|
147
|
+
// --- peer A: check knock inbox ---
|
|
148
|
+
await pageA.getByTestId("btn-share-playlist").click();
|
|
149
|
+
await pageA.getByTestId("share-panel").waitFor({ timeout: 5000 });
|
|
150
|
+
|
|
151
|
+
// wait for the knock inbox to appear on A's panel
|
|
152
|
+
await expect(pageA.getByTestId("knock-inbox")).toBeVisible({
|
|
153
|
+
timeout: 60_000,
|
|
154
|
+
});
|
|
155
|
+
logTs("[e2e] peerA: knock inbox has pending request");
|
|
156
|
+
|
|
157
|
+
// accept the knock (accept all docs)
|
|
158
|
+
const acceptBtn = pageA.getByRole("button", { name: /accept/ }).first();
|
|
159
|
+
await acceptBtn.click();
|
|
160
|
+
logTs("[e2e] peerA: accepted collaboration request");
|
|
161
|
+
|
|
162
|
+
// inbox should clear
|
|
163
|
+
await expect(pageA.getByTestId("knock-inbox")).not.toBeVisible({
|
|
164
|
+
timeout: 15_000,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// --- peer B: check if accepted via "check if accepted" button ---
|
|
168
|
+
// the status may already show granted if B received knock_notify
|
|
169
|
+
// otherwise click the check button
|
|
170
|
+
const statusEl = pageB.getByTestId("collab-request-status");
|
|
171
|
+
const alreadyGranted = await statusEl
|
|
172
|
+
.textContent({ timeout: 1000 })
|
|
173
|
+
.then((t) => t?.includes("access granted"))
|
|
174
|
+
.catch(() => false);
|
|
175
|
+
|
|
176
|
+
if (!alreadyGranted) {
|
|
177
|
+
// the "check if accepted" button appears in the outbound knocks section
|
|
178
|
+
const checkBtn = pageB.getByRole("button", { name: "check if accepted" });
|
|
179
|
+
if (await checkBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
180
|
+
await checkBtn.click();
|
|
181
|
+
logTs("[e2e] peerB: clicked check if accepted");
|
|
182
|
+
}
|
|
183
|
+
await expect(statusEl).toContainText("access granted", {
|
|
184
|
+
timeout: 30_000,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
logTs("[e2e] peerB: access granted confirmed");
|
|
188
|
+
} finally {
|
|
189
|
+
await Promise.allSettled([ctxA.close(), ctxB.close()]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// -----------------------------------------------------------------------
|
|
195
|
+
// collaboration request - denied
|
|
196
|
+
// -----------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
test(
|
|
199
|
+
"peer A denies collaboration request, peer B sees denied @p2p",
|
|
200
|
+
async ({ browser }) => {
|
|
201
|
+
test.setTimeout(600_000);
|
|
202
|
+
|
|
203
|
+
const ctxA = await browser.newContext();
|
|
204
|
+
const ctxB = await browser.newContext();
|
|
205
|
+
const pageA = await ctxA.newPage();
|
|
206
|
+
const pageB = await ctxB.newPage();
|
|
207
|
+
|
|
208
|
+
const fwd =
|
|
209
|
+
(tag: string) => (msg: import("@playwright/test").ConsoleMessage) => {
|
|
210
|
+
logTs(`[${tag}] ${msg.text()}`);
|
|
211
|
+
};
|
|
212
|
+
pageA.on("console", fwd("peerA"));
|
|
213
|
+
pageB.on("console", fwd("peerB"));
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
await resetAppState(pageA);
|
|
217
|
+
await createPlaylistViaUI(pageA);
|
|
218
|
+
await pageA.getByTestId("input-playlist-title").fill("collab-test-deny");
|
|
219
|
+
await pageA.getByTestId("input-playlist-title").blur();
|
|
220
|
+
await pageA.waitForTimeout(300);
|
|
221
|
+
|
|
222
|
+
const shareUrl = await enableP2PAndGetShareLink(pageA, "peerA");
|
|
223
|
+
await pageA.getByTestId("btn-mode-public").click();
|
|
224
|
+
await pageA.getByTestId("btn-share-playlist").click();
|
|
225
|
+
|
|
226
|
+
await resetAppState(pageB);
|
|
227
|
+
await createPlaylistViaUI(pageB);
|
|
228
|
+
await enableP2PAndGetShareLink(pageB, "peerB");
|
|
229
|
+
await pageB.getByTestId("btn-share-playlist").click();
|
|
230
|
+
|
|
231
|
+
await subscribeToPublicPlaylist(
|
|
232
|
+
pageB,
|
|
233
|
+
shareUrl,
|
|
234
|
+
"peerB",
|
|
235
|
+
"collab-test-deny"
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// B requests access
|
|
239
|
+
await pageB.getByTestId("btn-share-playlist").click();
|
|
240
|
+
await pageB.getByTestId("share-panel").waitFor({ timeout: 5000 });
|
|
241
|
+
await pageB.getByTestId("btn-request-collab-access").click();
|
|
242
|
+
logTs("[e2e] peerB: sent collaboration request");
|
|
243
|
+
|
|
244
|
+
await expect(pageB.getByTestId("collab-request-status")).toContainText(
|
|
245
|
+
"waiting for owner approval",
|
|
246
|
+
{ timeout: 30_000 }
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// A denies
|
|
250
|
+
await pageA.getByTestId("btn-share-playlist").click();
|
|
251
|
+
await pageA.getByTestId("share-panel").waitFor({ timeout: 5000 });
|
|
252
|
+
await expect(pageA.getByTestId("knock-inbox")).toBeVisible({
|
|
253
|
+
timeout: 60_000,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const denyBtn = pageA.getByRole("button", { name: "deny" }).first();
|
|
257
|
+
await denyBtn.click();
|
|
258
|
+
logTs("[e2e] peerA: denied collaboration request");
|
|
259
|
+
|
|
260
|
+
// B retries to get denied status
|
|
261
|
+
const checkBtn = pageB.getByRole("button", { name: "check if accepted" });
|
|
262
|
+
if (await checkBtn.isVisible({ timeout: 10_000 }).catch(() => false)) {
|
|
263
|
+
await checkBtn.click();
|
|
264
|
+
await expect(pageB.getByTestId("collab-request-status")).toContainText(
|
|
265
|
+
"access denied",
|
|
266
|
+
{ timeout: 30_000 }
|
|
267
|
+
);
|
|
268
|
+
logTs("[e2e] peerB: denied status confirmed");
|
|
269
|
+
}
|
|
270
|
+
} finally {
|
|
271
|
+
await Promise.allSettled([ctxA.close(), ctxB.close()]);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// -----------------------------------------------------------------------
|
|
277
|
+
// collaborative mode - auto-accept
|
|
278
|
+
// -----------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
test(
|
|
281
|
+
"collaborative mode auto-accepts edit request from subscriber @p2p",
|
|
282
|
+
async ({ browser }) => {
|
|
283
|
+
test.setTimeout(600_000);
|
|
284
|
+
|
|
285
|
+
const ctxA = await browser.newContext();
|
|
286
|
+
const ctxB = await browser.newContext();
|
|
287
|
+
const pageA = await ctxA.newPage();
|
|
288
|
+
const pageB = await ctxB.newPage();
|
|
289
|
+
|
|
290
|
+
const fwd =
|
|
291
|
+
(tag: string) => (msg: import("@playwright/test").ConsoleMessage) => {
|
|
292
|
+
logTs(`[${tag}] ${msg.text()}`);
|
|
293
|
+
};
|
|
294
|
+
pageA.on("console", fwd("peerA"));
|
|
295
|
+
pageB.on("console", fwd("peerB"));
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
await resetAppState(pageA);
|
|
299
|
+
await createPlaylistViaUI(pageA);
|
|
300
|
+
await pageA
|
|
301
|
+
.getByTestId("input-playlist-title")
|
|
302
|
+
.fill("collab-test-auto");
|
|
303
|
+
await pageA.getByTestId("input-playlist-title").blur();
|
|
304
|
+
await pageA.waitForTimeout(300);
|
|
305
|
+
|
|
306
|
+
const shareUrl = await enableP2PAndGetShareLink(pageA, "peerA");
|
|
307
|
+
|
|
308
|
+
// set mode to public + enable collaborative editing
|
|
309
|
+
await pageA.getByTestId("btn-mode-public").click();
|
|
310
|
+
await pageA.getByTestId("btn-toggle-collaborative").click();
|
|
311
|
+
await expect(
|
|
312
|
+
pageA.getByTestId("btn-toggle-collaborative")
|
|
313
|
+
).toHaveAttribute("aria-pressed", "true");
|
|
314
|
+
logTs("[e2e] peerA: mode=public, collaborative=on");
|
|
315
|
+
|
|
316
|
+
await pageA.getByTestId("btn-share-playlist").click();
|
|
317
|
+
|
|
318
|
+
// peer B subscribes
|
|
319
|
+
await resetAppState(pageB);
|
|
320
|
+
await createPlaylistViaUI(pageB);
|
|
321
|
+
await enableP2PAndGetShareLink(pageB, "peerB");
|
|
322
|
+
await pageB.getByTestId("btn-share-playlist").click();
|
|
323
|
+
|
|
324
|
+
await subscribeToPublicPlaylist(
|
|
325
|
+
pageB,
|
|
326
|
+
shareUrl,
|
|
327
|
+
"peerB",
|
|
328
|
+
"collab-test-auto"
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// B requests collaboration access
|
|
332
|
+
await pageB.getByTestId("btn-share-playlist").click();
|
|
333
|
+
await pageB.getByTestId("share-panel").waitFor({ timeout: 5000 });
|
|
334
|
+
await pageB.getByTestId("btn-request-collab-access").click();
|
|
335
|
+
logTs("[e2e] peerB: sent collaboration request (should auto-accept)");
|
|
336
|
+
|
|
337
|
+
// should get immediate "access granted" since collaborative is on
|
|
338
|
+
await expect(pageB.getByTestId("collab-request-status")).toContainText(
|
|
339
|
+
"access granted",
|
|
340
|
+
{ timeout: 60_000 }
|
|
341
|
+
);
|
|
342
|
+
logTs("[e2e] peerB: access granted automatically");
|
|
343
|
+
|
|
344
|
+
// no knock should appear in A's inbox (auto-accepted server-side)
|
|
345
|
+
// wait a moment then confirm knock inbox is absent
|
|
346
|
+
await pageA.getByTestId("btn-share-playlist").click();
|
|
347
|
+
await pageA.getByTestId("share-panel").waitFor({ timeout: 5000 });
|
|
348
|
+
await expect(pageA.getByTestId("knock-inbox")).not.toBeVisible({
|
|
349
|
+
timeout: 5000,
|
|
350
|
+
});
|
|
351
|
+
logTs("[e2e] peerA: knock inbox empty (auto-accepted, no pending knocks)");
|
|
352
|
+
} finally {
|
|
353
|
+
await Promise.allSettled([ctxA.close(), ctxB.close()]);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
);
|