@ferocia-oss/loki-target-chrome-docker 0.31.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of @ferocia-oss/loki-target-chrome-docker might be problematic. Click here for more details.

package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Joel Arvidsson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@ferocia-oss/loki-target-chrome-docker",
3
+ "version": "0.31.0",
4
+ "description": "Loki Chrome docker target",
5
+ "keywords": [
6
+ "loki"
7
+ ],
8
+ "homepage": "https://github.com/oblador/loki/tree/master/packages/target-chrome-docker",
9
+ "bugs": {
10
+ "url": "https://github.com/oblador/loki/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/oblador/loki.git",
15
+ "directory": "packages/target-chrome-docker"
16
+ },
17
+ "license": "MIT",
18
+ "files": [
19
+ "src"
20
+ ],
21
+ "main": "src/index.js",
22
+ "dependencies": {
23
+ "@ferocia-oss/loki-core": "^0.31.0",
24
+ "@ferocia-oss/loki-target-chrome-core": "^0.31.0",
25
+ "chrome-remote-interface": "^0.29.0",
26
+ "debug": "^4.1.1",
27
+ "execa": "^5.0.0",
28
+ "fs-extra": "^9.1.0",
29
+ "get-port": "^5.1.1",
30
+ "wait-on": "^5.2.1"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "gitHead": "d01d38e10afcd368dee3d07311e7a48b155d6a34"
36
+ }
@@ -0,0 +1,229 @@
1
+ const debug = require('debug')('loki:chrome:docker');
2
+ const { execSync } = require('child_process');
3
+ const execa = require('execa');
4
+ const waitOn = require('wait-on');
5
+ const CDP = require('chrome-remote-interface');
6
+ const getRandomPort = require('get-port');
7
+ const {
8
+ ChromeError,
9
+ ensureDependencyAvailable,
10
+ getAbsoluteURL,
11
+ } = require('@ferocia-oss/loki-core');
12
+ const { createChromeTarget } = require('@ferocia-oss/loki-target-chrome-core');
13
+ const { getLocalIPAddress } = require('./get-local-ip-address');
14
+ const { getNetworkHost } = require('./get-network-host');
15
+
16
+ const getExecutor = (dockerWithSudo) => (dockerPath, args) => {
17
+ if (dockerWithSudo) {
18
+ return execa('sudo', [dockerPath, ...args]);
19
+ }
20
+
21
+ return execa(dockerPath, args);
22
+ };
23
+
24
+ const waitOnCDPAvailable = (host, port) =>
25
+ new Promise((resolve, reject) => {
26
+ waitOn(
27
+ {
28
+ resources: [`tcp:${host}:${port}`],
29
+ delay: 50,
30
+ interval: 100,
31
+ timeout: 5000,
32
+ },
33
+ (err) => {
34
+ if (err) {
35
+ reject(err);
36
+ } else {
37
+ resolve();
38
+ }
39
+ }
40
+ );
41
+ });
42
+
43
+ function createChromeDockerTarget({
44
+ baseUrl = 'http://localhost:6006',
45
+ chromeDockerImage = 'yukinying/chrome-headless-browser-stable',
46
+ chromeFlags = ['--headless', '--disable-gpu', '--hide-scrollbars'],
47
+ dockerNet = null,
48
+ dockerWithSudo = false,
49
+ chromeDockerUseCopy = false,
50
+ chromeDockerWithoutSeccomp = false,
51
+ }) {
52
+ let port;
53
+ let dockerId;
54
+ let host;
55
+ let localPath;
56
+ let dockerUrl = getAbsoluteURL(baseUrl);
57
+ const isLocalFile = dockerUrl.indexOf('file:') === 0;
58
+ const staticMountPath = '/var/loki';
59
+ const dockerPath = 'docker';
60
+ const runArgs = ['run', '--rm', '-d', '-P'];
61
+ const execute = getExecutor(dockerWithSudo);
62
+
63
+ if (!chromeDockerWithoutSeccomp) {
64
+ runArgs.push(`--security-opt=seccomp=${__dirname}/docker-seccomp.json`);
65
+ }
66
+
67
+ if (dockerUrl.indexOf('http://localhost') === 0) {
68
+ const ip = getLocalIPAddress();
69
+ if (!ip) {
70
+ throw new Error(
71
+ 'Unable to detect local IP address, try passing --host argument'
72
+ );
73
+ }
74
+ dockerUrl = dockerUrl.replace('localhost', ip);
75
+ } else if (isLocalFile) {
76
+ localPath = dockerUrl.substr('file:'.length);
77
+ dockerUrl = `file://${staticMountPath}`;
78
+ if (!chromeDockerUseCopy) {
79
+ // setup volume mount if we're not using copy
80
+ runArgs.push('-v');
81
+ runArgs.push(`${localPath}:${staticMountPath}`);
82
+ }
83
+ }
84
+
85
+ async function getIsImageDownloaded(imageName) {
86
+ const { exitCode, stdout, stderr } = await execute(dockerPath, [
87
+ 'images',
88
+ '-q',
89
+ imageName,
90
+ ]);
91
+
92
+ if (exitCode !== 0) {
93
+ throw new Error(`Failed querying docker, ${stderr}`);
94
+ }
95
+ return stdout.trim().length !== 0;
96
+ }
97
+
98
+ async function copyFiles() {
99
+ const { exitCode, stdout, stderr } = await execute(dockerPath, [
100
+ 'cp',
101
+ localPath,
102
+ `${dockerId}:${staticMountPath}`,
103
+ ]);
104
+
105
+ if (exitCode !== 0) {
106
+ throw new Error(`Failed to copy files, ${stderr}`);
107
+ }
108
+ return stdout.trim().length !== 0;
109
+ }
110
+
111
+ async function ensureImageDownloaded() {
112
+ ensureDependencyAvailable('docker');
113
+
114
+ const isImageDownloaded = await getIsImageDownloaded(chromeDockerImage);
115
+ if (!isImageDownloaded) {
116
+ await execute(dockerPath, ['pull', chromeDockerImage]);
117
+ }
118
+ }
119
+
120
+ async function start() {
121
+ port = await getRandomPort();
122
+
123
+ ensureDependencyAvailable('docker');
124
+ const dockerArgs = runArgs.concat([
125
+ '--shm-size=1g',
126
+ '-p',
127
+ `${port}:${port}`,
128
+ ]);
129
+
130
+ if (dockerNet) {
131
+ dockerArgs.push(`--net=${dockerNet}`);
132
+ }
133
+ dockerArgs.push(chromeDockerImage);
134
+
135
+ const args = dockerArgs
136
+ .concat([
137
+ '--disable-datasaver-prompt',
138
+ '--no-first-run',
139
+ '--disable-extensions',
140
+ '--remote-debugging-address=0.0.0.0',
141
+ `--remote-debugging-port=${port}`,
142
+ ])
143
+ .concat(chromeFlags);
144
+
145
+ debug(
146
+ `Launching chrome in docker with command "${dockerPath} ${args.join(
147
+ ' '
148
+ )}"`
149
+ );
150
+ const { exitCode, stdout, stderr } = await execute(dockerPath, args);
151
+ if (exitCode === 0) {
152
+ dockerId = stdout;
153
+ if (chromeDockerUseCopy) {
154
+ await copyFiles();
155
+ }
156
+ const logs = execute(dockerPath, ['logs', dockerId, '--follow']);
157
+ const errorLogs = [];
158
+ logs.stderr.on('data', (chunk) => {
159
+ errorLogs.push(chunk);
160
+ });
161
+
162
+ host = await getNetworkHost(execute, dockerId);
163
+ try {
164
+ await waitOnCDPAvailable(host, port);
165
+ } catch (error) {
166
+ if (
167
+ error.message.startsWith('Timed out waiting for') &&
168
+ errorLogs.length !== 0
169
+ ) {
170
+ throw new ChromeError(
171
+ `Chrome failed to start with ${
172
+ errorLogs.length === 1 ? 'error' : 'errors'
173
+ } ${errorLogs
174
+ .map((e) => `"${e.toString('utf8').trim()}"`)
175
+ .join(', ')}`
176
+ );
177
+ }
178
+ throw error;
179
+ } finally {
180
+ if (logs.exitCode === null && !logs.killed) {
181
+ logs.kill();
182
+ }
183
+ }
184
+ debug(`Docker started with id ${dockerId}`);
185
+ } else {
186
+ throw new Error(`Failed starting docker, ${stderr}`);
187
+ }
188
+ }
189
+
190
+ async function stop() {
191
+ if (dockerId) {
192
+ debug(`Killing chrome docker instance with id ${dockerId}`);
193
+ await execute(dockerPath, ['kill', dockerId]);
194
+ } else {
195
+ debug('No chrome docker instance to kill');
196
+ }
197
+ }
198
+
199
+ async function createNewDebuggerInstance() {
200
+ debug(`Launching new tab with debugger at port ${host}:${port}`);
201
+ const target = await CDP.New({ host, port });
202
+ debug(`Launched with target id ${target.id}`);
203
+ const client = await CDP({ host, port, target });
204
+
205
+ client.close = () => {
206
+ debug('Closing tab');
207
+ return CDP.Close({ host, port, id: target.id });
208
+ };
209
+
210
+ return client;
211
+ }
212
+
213
+ process.on('SIGINT', () => {
214
+ if (dockerId) {
215
+ const maybeSudo = dockerWithSudo ? 'sudo ' : '';
216
+ execSync(`${maybeSudo}${dockerPath} kill ${dockerId}`);
217
+ }
218
+ });
219
+
220
+ return createChromeTarget(
221
+ start,
222
+ stop,
223
+ createNewDebuggerInstance,
224
+ dockerUrl,
225
+ ensureImageDownloaded
226
+ );
227
+ }
228
+
229
+ module.exports = { createChromeDockerTarget };
@@ -0,0 +1,128 @@
1
+ const { createChromeDockerTarget } = require('.');
2
+
3
+ const DOCKER_TEST_TIMEOUT = 120000;
4
+
5
+ const fetchStorybookUrl = async (baseUrl) => {
6
+ const target = createChromeDockerTarget({ baseUrl });
7
+ await target.start();
8
+ let result;
9
+ try {
10
+ result = await target.getStorybook({ baseUrl });
11
+ } catch (err) {
12
+ result = err;
13
+ }
14
+ await target.stop();
15
+ if (result instanceof Error) {
16
+ throw result;
17
+ }
18
+ return result;
19
+ };
20
+
21
+ const fetchStorybookFixture = async (fixture) =>
22
+ fetchStorybookUrl(`file:${__dirname}/../../../fixtures/storybook-${fixture}`);
23
+
24
+ const storybook = [
25
+ {
26
+ id: 'example-button--large',
27
+ kind: 'Example/Button',
28
+ story: 'Large',
29
+ parameters: {
30
+ args: {
31
+ label: 'Button',
32
+ size: 'large',
33
+ },
34
+ fileName: './src/stories/Button.stories.jsx',
35
+ globals: {
36
+ measureEnabled: false,
37
+ outline: false,
38
+ },
39
+ },
40
+ },
41
+ {
42
+ id: 'example-button--primary',
43
+ kind: 'Example/Button',
44
+ story: 'Primary',
45
+ parameters: {
46
+ args: {
47
+ label: 'Button',
48
+ primary: true,
49
+ },
50
+ fileName: './src/stories/Button.stories.jsx',
51
+ globals: {
52
+ measureEnabled: false,
53
+ outline: false,
54
+ },
55
+ },
56
+ },
57
+ {
58
+ id: 'example-button--secondary',
59
+ kind: 'Example/Button',
60
+ story: 'Secondary',
61
+ parameters: {
62
+ args: {
63
+ label: 'Button',
64
+ },
65
+ fileName: './src/stories/Button.stories.jsx',
66
+ globals: {
67
+ measureEnabled: false,
68
+ outline: false,
69
+ },
70
+ },
71
+ },
72
+ {
73
+ id: 'example-button--small',
74
+ kind: 'Example/Button',
75
+ story: 'Small',
76
+ parameters: {
77
+ args: {
78
+ label: 'Button',
79
+ size: 'small',
80
+ },
81
+ fileName: './src/stories/Button.stories.jsx',
82
+ globals: {
83
+ measureEnabled: false,
84
+ outline: false,
85
+ },
86
+ },
87
+ },
88
+ ];
89
+
90
+ describe('createChromeTarget', () => {
91
+ describe('.getStorybook', () => {
92
+ it(
93
+ 'fetches stories from webpack dynamic bundles',
94
+ async () => {
95
+ expect(await fetchStorybookFixture('dynamic')).toEqual(storybook);
96
+ },
97
+ DOCKER_TEST_TIMEOUT
98
+ );
99
+
100
+ it(
101
+ 'fetches stories from static bundles',
102
+ async () => {
103
+ expect(await fetchStorybookFixture('static')).toEqual(storybook);
104
+ },
105
+ DOCKER_TEST_TIMEOUT
106
+ );
107
+
108
+ it(
109
+ 'throws if not configured',
110
+ async () => {
111
+ await expect(fetchStorybookFixture('unconfigured')).rejects.toThrow(
112
+ "Unable to get stories. Try adding `import 'loki/configure-react'` to your .storybook/preview.js file."
113
+ );
114
+ },
115
+ DOCKER_TEST_TIMEOUT
116
+ );
117
+
118
+ it(
119
+ 'throws if not running',
120
+ async () => {
121
+ await expect(
122
+ fetchStorybookUrl('http://localhost:23456')
123
+ ).rejects.toThrow('Failed fetching stories because the server is down');
124
+ },
125
+ DOCKER_TEST_TIMEOUT
126
+ );
127
+ });
128
+ });