generated from storybookjs/addon-kit
/
setup-page.ts
211 lines (176 loc) Β· 7.68 KB
/
setup-page.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
//@ts-nocheck
const sanitizeURL = (url) => {
let finalURL = url;
// prepend URL protocol if not there
if (finalURL.indexOf('http://') === -1 && finalURL.indexOf('https://') === -1) {
finalURL = 'http://' + finalURL;
}
// remove iframe.html if present
finalURL = finalURL.replace(/iframe.html\s*$/, '');
// remove index.html if present
finalURL = finalURL.replace(/index.html\s*$/, '');
// add forward slash at the end if not there
if (finalURL.slice(-1) !== '/') {
finalURL = finalURL + '/';
}
return finalURL;
};
export const setupPage = async (page) => {
const targetURL = new URL('iframe.html', process.env.TARGET_URL).toString();
const viewMode = process.env.VIEW_MODE || 'story';
const renderedEvent = viewMode === 'docs' ? 'docsRendered' : 'storyRendered';
const referenceURL = process.env.REFERENCE_URL && sanitizeURL(process.env.REFERENCE_URL);
const debugPrintLimit = process.env.DEBUG_PRINT_LIMIT
? Number(process.env.DEBUG_PRINT_LIMIT)
: 1000;
if ('TARGET_URL' in process.env && !process.env.TARGET_URL) {
console.log(
`Received TARGET_URL but with a falsy value: ${process.env.TARGET_URL}, will fallback to ${targetURL} instead.`
);
}
await page.goto(targetURL, { waitUntil: 'load' }).catch((err) => {
if (err.message?.includes('ERR_CONNECTION_REFUSED')) {
const errorMessage = `Could not access the Storybook instance at ${targetURL}. Are you sure it's running?\n\n${err.message}`;
throw new Error(errorMessage);
}
throw err;
}); // FIXME: configure
// if we ever want to log something from the browser to node
await page.exposeBinding('logToPage', (_, message) => console.log(message));
await page.addScriptTag({
content: `
// colorizes the console output
const bold = (message) => \`\\u001b[1m\${message}\\u001b[22m\`;
const magenta = (message) => \`\\u001b[35m\${message}\\u001b[39m\`;
const blue = (message) => \`\\u001b[34m\${message}\\u001b[39m\`;
const red = (message) => \`\\u001b[31m\${message}\\u001b[39m\`;
const yellow = (message) => \`\\u001b[33m\${message}\\u001b[39m\`;
// removes circular references from the object
function serializer(replacer, cycleReplacer) {
let stack = [],
keys = [];
if (cycleReplacer == null)
cycleReplacer = function (_key, value) {
if (stack[0] === value) return '[Circular]';
return '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']';
};
return function (key, value) {
if (stack.length > 0) {
let thisPos = stack.indexOf(this);
~thisPos ? stack.splice(thisPos + 1) : stack.push(this);
~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key);
if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value);
} else {
stack.push(value);
}
return replacer == null ? value : replacer.call(this, key, value);
};
}
function safeStringify(obj, replacer, spaces, cycleReplacer) {
return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces);
}
function composeMessage(args) {
if (typeof args === 'undefined') return "undefined";
if (typeof args === 'string') return args;
return safeStringify(args);
}
function truncate(input, limit) {
if (input.length > limit) {
return input.substring(0, limit) + 'β¦';
}
return input;
}
class StorybookTestRunnerError extends Error {
constructor(storyId, errorMessage, logs) {
super(errorMessage);
this.name = 'StorybookTestRunnerError';
const storyUrl = \`${referenceURL || targetURL}?path=/story/\${storyId}\`;
const finalStoryUrl = \`\${storyUrl}&addonPanel=storybook/interactions/panel\`;
const separator = '\\n\\n--------------------------------------------------';
const extraLogs = logs.length > 0 ? separator + "\\n\\nBrowser logs:\\n\\n"+ logs.join('\\n\\n') : '';
this.message = \`\nAn error occurred in the following story. Access the link for full output:\n\${finalStoryUrl}\n\nMessage:\n \${truncate(errorMessage,${debugPrintLimit})}\n\${extraLogs}\`;
}
}
async function __throwError(storyId, errorMessage, logs) {
throw new StorybookTestRunnerError(storyId, errorMessage, logs);
}
async function __waitForElement(selector) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject();
}, 10000);
if (document.querySelector(selector)) {
clearTimeout(timeout);
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(mutations => {
if (document.querySelector(selector)) {
clearTimeout(timeout);
resolve(document.querySelector(selector));
observer.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
async function __getContext(storyId) {
return globalThis.__STORYBOOK_PREVIEW__.storyStore.loadStory({ storyId });
}
async function __test(storyId) {
try {
await __waitForElement('#root');
} catch(err) {
const message = \`Timed out waiting for Storybook to load after 10 seconds. Are you sure the Storybook is running correctly in that URL? Is the Storybook private (e.g. under authentication layers)?\n\n\nHTML: \${document.body.innerHTML}\`;
throw new StorybookTestRunnerError(storyId, message);
}
const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__;
if(!channel) {
throw new StorybookTestRunnerError(
storyId,
'The test runner could not access the Storybook channel. Are you sure the Storybook is running correctly in that URL?'
);
}
// collect logs to show upon test error
let logs = [];
const spyOnConsole = (method, name) => {
const originalFn = console[method];
return function () {
const message = [...arguments].map(composeMessage).join(', ');
const prefix = \`\${bold(name)}: \`;
logs.push(prefix + message);
originalFn.apply(console, arguments);
};
};
// console methods + color function for their prefix
const spiedMethods = {
log: blue,
warn: yellow,
error: red,
trace: magenta,
group: magenta,
groupCollapsed: magenta,
}
Object.entries(spiedMethods).forEach(([method, color]) => {
console[method] = spyOnConsole(method, color(method))
})
return new Promise((resolve, reject) => {
channel.on('${renderedEvent}', () => resolve(document.getElementById('root')));
channel.on('storyUnchanged', () => resolve(document.getElementById('root')));
channel.on('storyErrored', ({ description }) => reject(
new StorybookTestRunnerError(storyId, description, logs))
);
channel.on('storyThrewException', (error) => reject(
new StorybookTestRunnerError(storyId, error.message, logs))
);
channel.on('storyMissing', (id) => id === storyId && reject(
new StorybookTestRunnerError(storyId, 'The story was missing when trying to access it.', logs))
);
channel.emit('setCurrentStory', { storyId, viewMode: '${viewMode}' });
});
};
`,
});
};