@netlify/spark-ui 1.31.0-alpha.3 → 1.31.0-alpha.5
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/dist/components/index.d.ts +2 -2
- package/dist/components/preact/DropZone/DropZone.d.ts +27 -9
- package/dist/components/preact/DropZone/DropZone.js +59 -54
- package/dist/components/preact/DropZone/DropZone.test.d.ts +1 -0
- package/dist/components/preact/DropZone/index.d.ts +3 -3
- package/dist/components/preact/DropZone/index.js +29 -27
- package/dist/components/preact/DropZone/useDropDeploy.d.ts +9 -6
- package/dist/components/preact/DropZone/useDropDeploy.js +182 -131
- package/dist/components/preact/DropZone/useDropDeploy.test.d.ts +1 -0
- package/dist/components/preact/index.d.ts +2 -2
- package/dist/components/preact/index.js +24 -18
- package/dist/drop-core/analytics.d.ts +107 -0
- package/dist/drop-core/analytics.js +21 -0
- package/dist/drop-core/analytics.test.d.ts +1 -0
- package/dist/drop-core/authedApi.d.ts +45 -0
- package/dist/drop-core/authedApi.js +30 -0
- package/dist/drop-core/authedApi.test.d.ts +1 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/authedDropClient.d.ts +4 -11
- package/dist/drop-core/authedDropClient.js +100 -0
- package/dist/drop-core/authedDropClient.test.d.ts +1 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/buildClient.d.ts +5 -13
- package/dist/drop-core/buildClient.js +85 -0
- package/dist/drop-core/buildClient.test.d.ts +1 -0
- package/dist/drop-core/buildStash.d.ts +37 -0
- package/dist/drop-core/buildStash.js +84 -0
- package/dist/drop-core/buildStash.test.d.ts +0 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/client.d.ts +3 -1
- package/dist/drop-core/client.js +47 -0
- package/dist/drop-core/constants.d.ts +29 -0
- package/dist/drop-core/constants.js +10 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/deploy.d.ts +9 -0
- package/dist/drop-core/deploy.js +66 -0
- package/dist/drop-core/deploy.test.d.ts +1 -0
- package/dist/drop-core/detectBuild.test.d.ts +1 -0
- package/dist/drop-core/digest.test.d.ts +1 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/errors.d.ts +29 -0
- package/dist/drop-core/errors.js +119 -0
- package/dist/drop-core/errors.test.d.ts +1 -0
- package/dist/drop-core/fileTypes.test.d.ts +1 -0
- package/dist/drop-core/index.d.ts +25 -0
- package/dist/drop-core/index.js +47 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/readFiles.js +2 -2
- package/dist/drop-core/readFiles.test.d.ts +1 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/types.d.ts +10 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/zip.js +1 -1
- package/dist/drop-core/zip.test.d.ts +1 -0
- package/package.json +4 -1
- package/packages/components/index.ts +19 -0
- package/packages/components/preact/DropZone/DropZone.test.ts +71 -0
- package/packages/components/preact/DropZone/DropZone.tsx +29 -9
- package/packages/components/preact/DropZone/index.tsx +14 -3
- package/packages/components/preact/DropZone/useDropDeploy.test.ts +376 -0
- package/packages/components/preact/DropZone/useDropDeploy.ts +301 -145
- package/packages/components/preact/index.ts +19 -2
- package/packages/drop-core/README.md +91 -0
- package/packages/drop-core/analytics.test.ts +17 -0
- package/packages/drop-core/analytics.ts +122 -0
- package/packages/drop-core/authedApi.test.ts +100 -0
- package/packages/drop-core/authedApi.ts +81 -0
- package/packages/drop-core/authedDropClient.test.ts +196 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/authedDropClient.ts +30 -72
- package/packages/drop-core/buildClient.test.ts +112 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/buildClient.ts +27 -57
- package/packages/drop-core/buildStash.test.ts +85 -0
- package/packages/drop-core/buildStash.ts +185 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/client.ts +18 -6
- package/packages/drop-core/constants.ts +39 -0
- package/packages/drop-core/deploy.test.ts +177 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/deploy.ts +59 -9
- package/packages/drop-core/detectBuild.test.ts +139 -0
- package/packages/drop-core/digest.test.ts +24 -0
- package/packages/drop-core/errors.test.ts +98 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/errors.ts +68 -11
- package/packages/drop-core/fileTypes.test.ts +37 -0
- package/packages/drop-core/index.ts +44 -0
- package/packages/drop-core/readFiles.test.ts +124 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/types.ts +12 -6
- package/packages/drop-core/zip.test.ts +92 -0
- package/dist/components/preact/DropZone/drop-core/authedDropClient.js +0 -123
- package/dist/components/preact/DropZone/drop-core/buildClient.js +0 -99
- package/dist/components/preact/DropZone/drop-core/client.js +0 -45
- package/dist/components/preact/DropZone/drop-core/deploy.js +0 -48
- package/dist/components/preact/DropZone/drop-core/errors.js +0 -82
- package/dist/components/preact/DropZone/drop-core/index.d.ts +0 -11
- package/dist/components/preact/DropZone/drop-core/index.js +0 -36
- package/packages/components/preact/DropZone/drop-core/index.ts +0 -11
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/detectBuild.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/detectBuild.js +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/digest.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/digest.js +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/fileTypes.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/fileTypes.js +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/readFiles.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/types.js +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/zip.d.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/detectBuild.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/digest.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/fileTypes.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/readFiles.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/zip.ts +0 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { h, render } from 'preact';
|
|
4
|
+
import { useDropDeploy } from './useDropDeploy';
|
|
5
|
+
import {
|
|
6
|
+
DROPZONE_EVENTS,
|
|
7
|
+
NotAuthenticatedError,
|
|
8
|
+
type BuildClient,
|
|
9
|
+
type DropClient,
|
|
10
|
+
type ProcessedFile,
|
|
11
|
+
} from '../../../drop-core';
|
|
12
|
+
|
|
13
|
+
// The stash is unit-tested in drop-core/buildStash.test.ts; here it is mocked
|
|
14
|
+
// so the hook's orchestration (when it saves, loads, and clears) is what's
|
|
15
|
+
// under test — and so no IndexedDB shim is needed.
|
|
16
|
+
vi.mock('../../../drop-core', async importOriginal => {
|
|
17
|
+
const original = await importOriginal<typeof import('../../../drop-core')>();
|
|
18
|
+
return {
|
|
19
|
+
...original,
|
|
20
|
+
saveDropStash: vi.fn(async () => true),
|
|
21
|
+
loadDropStash: vi.fn(async () => null),
|
|
22
|
+
clearDropStash: vi.fn(async () => {}),
|
|
23
|
+
hasDropStashMarker: vi.fn(() => false),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
import { clearDropStash, hasDropStashMarker, loadDropStash, saveDropStash } from '../../../drop-core';
|
|
27
|
+
|
|
28
|
+
const encoder = new TextEncoder();
|
|
29
|
+
const file = (path: string, contents: string): ProcessedFile => {
|
|
30
|
+
const content = encoder.encode(contents).buffer as ArrayBuffer;
|
|
31
|
+
return { path, content, size: content.byteLength, sha: 'test-sha', name: path.split('/').pop()! };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const staticFiles = () => [file('/index.html', '<h1>hi</h1>'), file('/style.css', 'body{}')];
|
|
35
|
+
const buildFiles = () => [
|
|
36
|
+
file('/package.json', '{"scripts":{"build":"vite build"}}'),
|
|
37
|
+
file('/index.html', '<h1>hi</h1>'),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function makeAnonymousClient(log: string[]): DropClient {
|
|
41
|
+
return {
|
|
42
|
+
getToken: async () => (log.push('anon:getToken'), 'anon-token'),
|
|
43
|
+
createDeploy: async () => (
|
|
44
|
+
log.push('anon:createDeploy'),
|
|
45
|
+
{ id: 'anon-site-id', deploy_id: 'anon-deploy-id', subdomain: 'anon-sub', required: [] }
|
|
46
|
+
),
|
|
47
|
+
uploadFile: async () => void log.push('anon:uploadFile'),
|
|
48
|
+
waitUntilReady: async () => void log.push('anon:wait'),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeAuthedDropClient(log: string[], behavior: 'ok' | 'no-auth' | 'server-error'): DropClient {
|
|
53
|
+
return {
|
|
54
|
+
getToken: async () => {
|
|
55
|
+
log.push('authed:getToken');
|
|
56
|
+
if (behavior === 'no-auth') throw new NotAuthenticatedError();
|
|
57
|
+
return 'bearer-token';
|
|
58
|
+
},
|
|
59
|
+
createDeploy: async () => {
|
|
60
|
+
log.push('authed:createDeploy');
|
|
61
|
+
if (behavior === 'server-error')
|
|
62
|
+
throw Object.assign(new Error('deploy create failed: 500'), { status: 500 });
|
|
63
|
+
return { id: 'site-uuid', deploy_id: 'deploy-bson', subdomain: 'my-account-site', required: [] };
|
|
64
|
+
},
|
|
65
|
+
uploadFile: async () => void log.push('authed:uploadFile'),
|
|
66
|
+
waitUntilReady: async () => void log.push('authed:wait'),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makeBuildClient(behavior: 'ok' | 'no-auth'): BuildClient {
|
|
71
|
+
return {
|
|
72
|
+
createSite: async () => {
|
|
73
|
+
if (behavior === 'no-auth') throw new NotAuthenticatedError();
|
|
74
|
+
return { id: 'site-uuid', name: 'built-site' };
|
|
75
|
+
},
|
|
76
|
+
createBuildFromZip: async () => ({ id: 'build-id', deploy_id: 'deploy-bson' }),
|
|
77
|
+
waitUntilBuildReady: async () => {},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface HarnessResult {
|
|
82
|
+
api: ReturnType<typeof useDropDeploy>;
|
|
83
|
+
events: Array<{ event: string; props: Record<string, unknown> | undefined }>;
|
|
84
|
+
deploys: unknown[];
|
|
85
|
+
buildDeploys: unknown[];
|
|
86
|
+
errors: Array<{ message: string; reason: string }>;
|
|
87
|
+
handoffs: unknown[];
|
|
88
|
+
assigned: string[];
|
|
89
|
+
unmount: () => void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function mountHook(options: Partial<Parameters<typeof useDropDeploy>[0]> = {}): HarnessResult {
|
|
93
|
+
const result = {
|
|
94
|
+
events: [],
|
|
95
|
+
deploys: [],
|
|
96
|
+
buildDeploys: [],
|
|
97
|
+
errors: [],
|
|
98
|
+
handoffs: [],
|
|
99
|
+
assigned: [],
|
|
100
|
+
} as unknown as HarnessResult;
|
|
101
|
+
|
|
102
|
+
window.location.assign = ((url: string) => result.assigned.push(String(url))) as never;
|
|
103
|
+
|
|
104
|
+
function Harness() {
|
|
105
|
+
result.api = useDropDeploy({
|
|
106
|
+
claimUrl: name => `https://app.netlify.com/drop/${name}`,
|
|
107
|
+
siteDashboardUrl: name => `https://app.netlify.com/projects/${name}`,
|
|
108
|
+
buildSignupUrl: 'https://app.netlify.com/signup?next=/drop',
|
|
109
|
+
buildDeployUrl: build => `https://app.netlify.com/projects/${(build as { siteName: string }).siteName}`,
|
|
110
|
+
onDeploy: site => void result.deploys.push(site),
|
|
111
|
+
onBuildDeploy: build => void result.buildDeploys.push(build),
|
|
112
|
+
onError: error => void result.errors.push(error),
|
|
113
|
+
track: (event, props) => void result.events.push({ event, props }),
|
|
114
|
+
...options,
|
|
115
|
+
});
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const container = document.createElement('div');
|
|
120
|
+
render(h(Harness, {}), container);
|
|
121
|
+
result.unmount = () => render(null, container);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Preact effects and chained promises settle on real timers — poll briefly. */
|
|
126
|
+
async function waitUntil(condition: () => boolean, ms = 500) {
|
|
127
|
+
const start = Date.now();
|
|
128
|
+
while (!condition()) {
|
|
129
|
+
if (Date.now() - start > ms) throw new Error('condition never became true');
|
|
130
|
+
await new Promise(r => setTimeout(r, 5));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const settle = () => new Promise(r => setTimeout(r, 10));
|
|
135
|
+
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
localStorage.clear();
|
|
138
|
+
vi.mocked(saveDropStash).mockClear().mockResolvedValue(true);
|
|
139
|
+
vi.mocked(loadDropStash).mockClear().mockResolvedValue(null);
|
|
140
|
+
vi.mocked(clearDropStash).mockClear();
|
|
141
|
+
vi.mocked(hasDropStashMarker).mockClear().mockReturnValue(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
afterEach(() => vi.restoreAllMocks());
|
|
145
|
+
|
|
146
|
+
describe('static drops', () => {
|
|
147
|
+
it('deploys into the account and lands on the dashboard — no claim artifacts anywhere', async () => {
|
|
148
|
+
const log: string[] = [];
|
|
149
|
+
const harness = mountHook({
|
|
150
|
+
authenticatedDropClient: makeAuthedDropClient(log, 'ok'),
|
|
151
|
+
client: makeAnonymousClient(log),
|
|
152
|
+
});
|
|
153
|
+
await harness.api.deploy(async () => staticFiles(), 'drag', ['text/html']);
|
|
154
|
+
await settle();
|
|
155
|
+
|
|
156
|
+
expect(harness.assigned).toEqual(['https://app.netlify.com/projects/my-account-site']);
|
|
157
|
+
expect(localStorage.getItem('drop-token')).toBeNull();
|
|
158
|
+
expect(localStorage.getItem('dropSiteName')).toBeNull();
|
|
159
|
+
expect(harness.deploys[0]).toEqual({
|
|
160
|
+
url: 'https://my-account-site.netlify.app',
|
|
161
|
+
siteName: 'my-account-site',
|
|
162
|
+
siteId: 'site-uuid',
|
|
163
|
+
authenticated: true,
|
|
164
|
+
});
|
|
165
|
+
const succeeded = harness.events.find(e => e.event === DROPZONE_EVENTS.DEPLOY_SUCCEEDED);
|
|
166
|
+
expect(succeeded?.props).toEqual({
|
|
167
|
+
method: 'drag',
|
|
168
|
+
site_name: 'my-account-site',
|
|
169
|
+
file_count: 2,
|
|
170
|
+
authenticated: true,
|
|
171
|
+
file_types: ['text/html'],
|
|
172
|
+
});
|
|
173
|
+
expect(log.some(entry => entry.startsWith('anon:'))).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('falls back to the anonymous claim flow when the session is gone', async () => {
|
|
177
|
+
const log: string[] = [];
|
|
178
|
+
const harness = mountHook({
|
|
179
|
+
authenticatedDropClient: makeAuthedDropClient(log, 'no-auth'),
|
|
180
|
+
client: makeAnonymousClient(log),
|
|
181
|
+
});
|
|
182
|
+
await harness.api.deploy(async () => staticFiles(), 'drag', ['text/html']);
|
|
183
|
+
await settle();
|
|
184
|
+
|
|
185
|
+
expect(harness.assigned).toEqual(['https://app.netlify.com/drop/anon-sub#drop_token=anon-token']);
|
|
186
|
+
expect(localStorage.getItem('drop-token')).toBe('anon-token');
|
|
187
|
+
expect(localStorage.getItem('dropSiteName')).toBe('anon-sub');
|
|
188
|
+
const succeeded = harness.events.find(e => e.event === DROPZONE_EVENTS.DEPLOY_SUCCEEDED);
|
|
189
|
+
expect(succeeded?.props).toMatchObject({ authenticated: false, auth_fallback: true });
|
|
190
|
+
expect(log[0]).toBe('authed:getToken'); // authed tried first, then anonymous
|
|
191
|
+
expect(log).toContain('anon:getToken');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('surfaces a real authed failure as an error — never a silent anonymous duplicate', async () => {
|
|
195
|
+
const log: string[] = [];
|
|
196
|
+
const harness = mountHook({
|
|
197
|
+
authenticatedDropClient: makeAuthedDropClient(log, 'server-error'),
|
|
198
|
+
client: makeAnonymousClient(log),
|
|
199
|
+
});
|
|
200
|
+
await harness.api.deploy(async () => staticFiles(), 'drag', []);
|
|
201
|
+
await settle();
|
|
202
|
+
|
|
203
|
+
expect(harness.assigned).toEqual([]);
|
|
204
|
+
expect(harness.api.status.kind).toBe('error');
|
|
205
|
+
expect(harness.errors).toHaveLength(1);
|
|
206
|
+
expect(harness.events.some(e => e.event === DROPZONE_EVENTS.DEPLOY_FAILED)).toBe(true);
|
|
207
|
+
expect(log.some(entry => entry.startsWith('anon:'))).toBe(false);
|
|
208
|
+
expect(harness.deploys).toHaveLength(0);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('stays fully anonymous without authenticated wiring', async () => {
|
|
212
|
+
const harness = mountHook({ client: makeAnonymousClient([]) });
|
|
213
|
+
await harness.api.deploy(async () => staticFiles(), 'browse', []);
|
|
214
|
+
await settle();
|
|
215
|
+
|
|
216
|
+
expect(harness.assigned).toEqual(['https://app.netlify.com/drop/anon-sub#drop_token=anon-token']);
|
|
217
|
+
const succeeded = harness.events.find(e => e.event === DROPZONE_EVENTS.DEPLOY_SUCCEEDED);
|
|
218
|
+
expect(succeeded?.props).toMatchObject({ authenticated: false });
|
|
219
|
+
expect(succeeded?.props).not.toHaveProperty('auth_fallback');
|
|
220
|
+
expect(succeeded?.props).not.toHaveProperty('file_types'); // omitted, never []
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('reports an empty selection as a failure with file_count 0', async () => {
|
|
224
|
+
const harness = mountHook({ client: makeAnonymousClient([]) });
|
|
225
|
+
await harness.api.deploy(async () => [], 'drag', []);
|
|
226
|
+
expect(harness.api.status).toEqual({ kind: 'error', message: 'No files found to deploy.' });
|
|
227
|
+
const failed = harness.events.find(e => e.event === DROPZONE_EVENTS.DEPLOY_FAILED);
|
|
228
|
+
expect(failed?.props).toMatchObject({ file_count: 0 });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('emits FILES_DROPPED for drags only', async () => {
|
|
232
|
+
const dragged = mountHook({ client: makeAnonymousClient([]) });
|
|
233
|
+
await dragged.api.deploy(async () => staticFiles(), 'drag', []);
|
|
234
|
+
expect(dragged.events.some(e => e.event === DROPZONE_EVENTS.FILES_DROPPED)).toBe(true);
|
|
235
|
+
|
|
236
|
+
const browsed = mountHook({ client: makeAnonymousClient([]) });
|
|
237
|
+
await browsed.api.deploy(async () => staticFiles(), 'browse', []);
|
|
238
|
+
expect(browsed.events.some(e => e.event === DROPZONE_EVENTS.FILES_DROPPED)).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('build-required drops', () => {
|
|
243
|
+
it('builds in place for an authenticated visitor and lands on the project', async () => {
|
|
244
|
+
const harness = mountHook({ buildClient: makeBuildClient('ok'), getUploadToken: async () => 'tok' });
|
|
245
|
+
await harness.api.deploy(async () => buildFiles(), 'drag', []);
|
|
246
|
+
await settle();
|
|
247
|
+
|
|
248
|
+
expect(harness.assigned).toEqual(['https://app.netlify.com/projects/built-site']);
|
|
249
|
+
expect(harness.buildDeploys).toHaveLength(1);
|
|
250
|
+
expect(harness.events.map(e => e.event)).toContain(DROPZONE_EVENTS.BUILD_DEPLOY_SUCCEEDED);
|
|
251
|
+
expect(saveDropStash).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('stashes the archive and redirects to signup when there is no session', async () => {
|
|
255
|
+
const harness = mountHook({ buildClient: makeBuildClient('no-auth'), getUploadToken: async () => null });
|
|
256
|
+
await harness.api.deploy(async () => buildFiles(), 'drag', []);
|
|
257
|
+
await settle();
|
|
258
|
+
|
|
259
|
+
expect(saveDropStash).toHaveBeenCalledOnce();
|
|
260
|
+
const [archive] = vi.mocked(saveDropStash).mock.calls[0];
|
|
261
|
+
expect(archive.name).toBe('source.zip');
|
|
262
|
+
expect(harness.events.map(e => e.event)).toContain(DROPZONE_EVENTS.PROJECT_STASHED);
|
|
263
|
+
expect(harness.assigned).toEqual(['https://app.netlify.com/signup?next=/drop']);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('lets onBuildSignupHandoff own the hand-off instead of redirecting', async () => {
|
|
267
|
+
const handoffs: unknown[] = [];
|
|
268
|
+
const harness = mountHook({
|
|
269
|
+
buildClient: makeBuildClient('no-auth'),
|
|
270
|
+
getUploadToken: async () => null,
|
|
271
|
+
onBuildSignupHandoff: info => void handoffs.push(info),
|
|
272
|
+
});
|
|
273
|
+
await harness.api.deploy(async () => buildFiles(), 'drag', []);
|
|
274
|
+
await settle();
|
|
275
|
+
|
|
276
|
+
expect(harness.assigned).toEqual([]);
|
|
277
|
+
expect(handoffs).toEqual([
|
|
278
|
+
{ stashed: true, detection: { buildRequired: true, reason: 'package-json-build-script' } },
|
|
279
|
+
]);
|
|
280
|
+
expect(harness.api.status.kind).toBe('idle'); // zone stays usable behind the popup
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('redirects to signup without stashing when nothing here could resume', async () => {
|
|
284
|
+
const harness = mountHook({}); // no build wiring at all
|
|
285
|
+
await harness.api.deploy(async () => buildFiles(), 'drag', []);
|
|
286
|
+
await settle();
|
|
287
|
+
|
|
288
|
+
expect(saveDropStash).not.toHaveBeenCalled();
|
|
289
|
+
expect(harness.assigned).toEqual(['https://app.netlify.com/signup?next=/drop']);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('cedes everything to onBuildRequired when set', async () => {
|
|
293
|
+
const required: unknown[] = [];
|
|
294
|
+
const harness = mountHook({
|
|
295
|
+
buildClient: makeBuildClient('ok'),
|
|
296
|
+
getUploadToken: async () => 'tok',
|
|
297
|
+
onBuildRequired: info => void required.push(info),
|
|
298
|
+
});
|
|
299
|
+
await harness.api.deploy(async () => buildFiles(), 'drag', []);
|
|
300
|
+
await settle();
|
|
301
|
+
|
|
302
|
+
expect(required).toHaveLength(1);
|
|
303
|
+
expect(harness.assigned).toEqual([]);
|
|
304
|
+
expect(harness.api.status.kind).toBe('idle');
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('stash resume on mount', () => {
|
|
309
|
+
const stashRecord = () => ({
|
|
310
|
+
zipFile: new File(['zip'], 'source.zip', { type: 'application/zip' }),
|
|
311
|
+
buildSettings: null,
|
|
312
|
+
createdAt: Date.now(),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('deploys the stash for an authenticated visitor and consumes it', async () => {
|
|
316
|
+
vi.mocked(hasDropStashMarker).mockReturnValue(true);
|
|
317
|
+
vi.mocked(loadDropStash).mockResolvedValue(stashRecord());
|
|
318
|
+
const harness = mountHook({ buildClient: makeBuildClient('ok'), getUploadToken: async () => 'tok' });
|
|
319
|
+
|
|
320
|
+
await waitUntil(() => harness.assigned.length > 0);
|
|
321
|
+
expect(harness.assigned).toEqual(['https://app.netlify.com/projects/built-site']);
|
|
322
|
+
expect(harness.events.map(e => e.event)).toContain(DROPZONE_EVENTS.RESUMED);
|
|
323
|
+
expect(clearDropStash).toHaveBeenCalled();
|
|
324
|
+
expect(harness.buildDeploys).toHaveLength(1);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('keeps the stash when the visitor came back logged out', async () => {
|
|
328
|
+
vi.mocked(hasDropStashMarker).mockReturnValue(true);
|
|
329
|
+
vi.mocked(loadDropStash).mockResolvedValue(stashRecord());
|
|
330
|
+
const harness = mountHook({ buildClient: makeBuildClient('no-auth'), getUploadToken: async () => null });
|
|
331
|
+
|
|
332
|
+
await waitUntil(() => vi.mocked(loadDropStash).mock.calls.length > 0);
|
|
333
|
+
await settle();
|
|
334
|
+
expect(clearDropStash).not.toHaveBeenCalled(); // preserved for the next authenticated visit
|
|
335
|
+
expect(harness.api.status.kind).toBe('idle');
|
|
336
|
+
expect(harness.errors).toHaveLength(0);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('surfaces a real resume failure with re-drop copy, stash consumed', async () => {
|
|
340
|
+
vi.mocked(hasDropStashMarker).mockReturnValue(true);
|
|
341
|
+
vi.mocked(loadDropStash).mockResolvedValue(stashRecord());
|
|
342
|
+
const failing = makeBuildClient('ok');
|
|
343
|
+
failing.createBuildFromZip = async () => {
|
|
344
|
+
throw new Error('boom');
|
|
345
|
+
};
|
|
346
|
+
const harness = mountHook({ buildClient: failing, getUploadToken: async () => 'tok' });
|
|
347
|
+
|
|
348
|
+
await waitUntil(() => harness.api.status.kind === 'error');
|
|
349
|
+
expect(harness.api.status).toMatchObject({
|
|
350
|
+
kind: 'error',
|
|
351
|
+
message: 'We couldn’t automatically deploy your project. Drop it again to publish it.',
|
|
352
|
+
});
|
|
353
|
+
const failed = harness.events.find(e => e.event === DROPZONE_EVENTS.DEPLOY_FAILED);
|
|
354
|
+
expect(failed?.props).toEqual({ reason: 'resume_failed' });
|
|
355
|
+
expect(clearDropStash).toHaveBeenCalled();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('never attempts without the marker, wiring, or with resume disabled', async () => {
|
|
359
|
+
mountHook({ buildClient: makeBuildClient('ok'), getUploadToken: async () => 'tok' });
|
|
360
|
+
await settle();
|
|
361
|
+
expect(loadDropStash).not.toHaveBeenCalled(); // no marker
|
|
362
|
+
|
|
363
|
+
vi.mocked(hasDropStashMarker).mockReturnValue(true);
|
|
364
|
+
mountHook({}); // marker but no wiring
|
|
365
|
+
await settle();
|
|
366
|
+
expect(loadDropStash).not.toHaveBeenCalled();
|
|
367
|
+
|
|
368
|
+
mountHook({
|
|
369
|
+
buildClient: makeBuildClient('ok'),
|
|
370
|
+
getUploadToken: async () => 'tok',
|
|
371
|
+
resumeStashedBuilds: false,
|
|
372
|
+
});
|
|
373
|
+
await settle();
|
|
374
|
+
expect(loadDropStash).not.toHaveBeenCalled();
|
|
375
|
+
});
|
|
376
|
+
});
|