@excom/detect-browser 0.1.0

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 (35) hide show
  1. package/.rush/temp/chunked-rush-logs/detect-browser.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/detect-browser.build_docs.chunks.jsonl +1 -0
  3. package/.rush/temp/chunked-rush-logs/detect-browser.build_package-metas.chunks.jsonl +1 -0
  4. package/.rush/temp/operation/apply-exports/all.log +1 -0
  5. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  6. package/.rush/temp/operation/apply-exports/state.json +3 -0
  7. package/.rush/temp/operation/build_docs/all.log +1 -0
  8. package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
  9. package/.rush/temp/operation/build_docs/state.json +3 -0
  10. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  11. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  12. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  13. package/.rush/temp/shrinkwrap-deps.json +3 -0
  14. package/config/rig.json +5 -0
  15. package/detect-browser.ts +196 -0
  16. package/index.ts +17 -0
  17. package/package.json +42 -0
  18. package/rush-logs/detect-browser.apply-exports.cache.log +1 -0
  19. package/rush-logs/detect-browser.apply-exports.log +1 -0
  20. package/rush-logs/detect-browser.build_docs.cache.log +1 -0
  21. package/rush-logs/detect-browser.build_docs.log +1 -0
  22. package/rush-logs/detect-browser.build_package-metas.cache.log +1 -0
  23. package/rush-logs/detect-browser.build_package-metas.log +1 -0
  24. package/support/custom-elements.json +327 -0
  25. package/support/demos/language.html +16 -0
  26. package/support/demos/safari.html +11 -0
  27. package/support/demos/simple.html +15 -0
  28. package/support/dist-docs/detect-browser.md +145 -0
  29. package/support/docs/README.md +51 -0
  30. package/support/package-meta.json +146 -0
  31. package/support/tests/detect-browser.test.ts +413 -0
  32. package/support/tests/language.view.test.ts +21 -0
  33. package/support/tests/safari.view.test.ts +29 -0
  34. package/support/tests/simple.view.test.ts +20 -0
  35. package/tsconfig.json +5 -0
@@ -0,0 +1,413 @@
1
+ import {
2
+ afterEach,
3
+ describe,
4
+ expect,
5
+ fixture,
6
+ it,
7
+ vi,
8
+ wait,
9
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
10
+ import "../../index";
11
+
12
+ const UA = {
13
+ chromeMac:
14
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
15
+ edgeWin10:
16
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.91",
17
+ firefoxWin7:
18
+ "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0",
19
+ firefoxIos:
20
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/121.0 Mobile/15E148 Safari/605.1.15",
21
+ safariMac:
22
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15",
23
+ safariIos:
24
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
25
+ safariIpad:
26
+ "Mozilla/5.0 (iPad; CPU OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1",
27
+ androidStock:
28
+ "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; SM-T230 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Safari/537.36",
29
+ chromeAndroid:
30
+ "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.144 Mobile Safari/537.36",
31
+ chromeLinux:
32
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
33
+ chromeIos:
34
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.6099.119 Mobile/15E148 Safari/604.1",
35
+ operaPresto:
36
+ "Opera/9.80 (Windows NT 6.3; WOW64) Presto/2.12.388 Version/12.18",
37
+ ie11Win8: "Mozilla/5.0 (Windows NT 6.2; Trident/7.0; rv:11.0) like Gecko",
38
+ ie10Win7: "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)",
39
+ // Contrived: a Gecko/ token alongside WebKit, only WebKit/Blink should win
40
+ geckoAndWebkit:
41
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Gecko/20100101 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
42
+ };
43
+
44
+ /** A full desktop-Chrome navigator; override fields per test. */
45
+ const stubNavigator = (overrides: Record<string, unknown> = {}) => {
46
+ vi.stubGlobal("navigator", {
47
+ userAgent: UA.chromeMac,
48
+ userAgentData: { platform: "macOS" },
49
+ vendor: "Google Inc.",
50
+ platform: "MacIntel",
51
+ language: "en-US",
52
+ languages: ["en-US", "en"],
53
+ cookieEnabled: true,
54
+ doNotTrack: "1",
55
+ standalone: false,
56
+ hardwareConcurrency: 16,
57
+ deviceMemory: 8,
58
+ ...overrides,
59
+ });
60
+ };
61
+
62
+ const mount = () =>
63
+ fixture<HTMLDetectBrowserElement>(`<detect-browser></detect-browser>`);
64
+
65
+ describe("detect-browser", () => {
66
+ afterEach(() => {
67
+ document.body.innerHTML = "";
68
+ vi.unstubAllGlobals();
69
+ vi.restoreAllMocks();
70
+ });
71
+
72
+ it("populates browser metadata", async () => {
73
+ stubNavigator();
74
+
75
+ const el = mount();
76
+ expect(el).dom.to.equalTag(
77
+ `<detect-browser
78
+ browser-engine="blink"
79
+ browser-name="chrome"
80
+ browser-version="120.0.0.0"
81
+ browser-vendor="google inc."
82
+ cookies-enabled
83
+ device-type="desktop"
84
+ do-not-track="1"
85
+ language-id="en-us"
86
+ operating-platform="macos"
87
+ operating-system="macos 10.15"
88
+ ></detect-browser>`,
89
+ );
90
+ expect(el.provision).toEqual(
91
+ expect.objectContaining({
92
+ userAgent: expect.any(String),
93
+ languageIds: ["en-US", "en"],
94
+ hardwareConcurrency: 16,
95
+ deviceMemory: 8,
96
+ }),
97
+ );
98
+ });
99
+
100
+ describe("browser / engine / OS detection", () => {
101
+ it("detects Edge on Windows 10 as Blink", () => {
102
+ stubNavigator({ userAgent: UA.edgeWin10, platform: "Win32" });
103
+ const el = mount();
104
+ expect(el.browserName).toBe("edge");
105
+ expect(el.browserVersion).toBe("120.0.2210.91");
106
+ expect(el.browserEngine).toBe("blink");
107
+ expect(el.operatingSystem).toBe("windows 10+");
108
+ expect(el.deviceType).toBe("desktop");
109
+ });
110
+
111
+ it("detects Firefox on Windows 7 as Gecko", () => {
112
+ stubNavigator({ userAgent: UA.firefoxWin7, vendor: "" });
113
+ const el = mount();
114
+ expect(el.browserName).toBe("firefox");
115
+ expect(el.browserVersion).toBe("121.0");
116
+ expect(el.browserEngine).toBe("gecko");
117
+ expect(el.operatingSystem).toBe("windows 7");
118
+ // empty vendor string is reported as absent
119
+ expect(el.hasAttribute("browser-vendor")).toBe(false);
120
+ expect(el.browserVendor).toBe(null);
121
+ });
122
+
123
+ it("detects Firefox for iOS (FxiOS) as WebKit on a mobile device", () => {
124
+ stubNavigator({ userAgent: UA.firefoxIos, vendor: "Apple Computer, Inc." });
125
+ const el = mount();
126
+ expect(el.browserName).toBe("firefox");
127
+ expect(el.browserVersion).toBe("121.0");
128
+ expect(el.browserEngine).toBe("webkit");
129
+ expect(el.operatingSystem).toBe("ios");
130
+ expect(el.deviceType).toBe("mobile");
131
+ });
132
+
133
+ it("detects Safari on macOS via the Version/ token", () => {
134
+ stubNavigator({
135
+ userAgent: UA.safariMac,
136
+ vendor: "Apple Computer, Inc.",
137
+ userAgentData: undefined,
138
+ });
139
+ const el = mount();
140
+ expect(el).dom.to.equalTag(
141
+ `<detect-browser
142
+ browser-engine="webkit"
143
+ browser-name="safari"
144
+ browser-version="16.5"
145
+ browser-vendor="apple computer, inc."
146
+ cookies-enabled
147
+ device-type="desktop"
148
+ do-not-track="1"
149
+ language-id="en-us"
150
+ operating-platform="macintel"
151
+ operating-system="macos 13.4"
152
+ ></detect-browser>`,
153
+ );
154
+ });
155
+
156
+ it("detects Safari on iPhone as iOS mobile", () => {
157
+ stubNavigator({
158
+ userAgent: UA.safariIos,
159
+ platform: "iPhone",
160
+ userAgentData: undefined,
161
+ });
162
+ const el = mount();
163
+ expect(el.browserName).toBe("safari");
164
+ expect(el.browserVersion).toBe("17.2");
165
+ expect(el.operatingSystem).toBe("ios");
166
+ expect(el.operatingPlatform).toBe("iphone");
167
+ expect(el.deviceType).toBe("mobile");
168
+ });
169
+
170
+ it("detects Safari on iPad as iOS", () => {
171
+ stubNavigator({ userAgent: UA.safariIpad });
172
+ const el = mount();
173
+ expect(el.browserName).toBe("safari");
174
+ expect(el.operatingSystem).toBe("ios");
175
+ expect(el.deviceType).toBe("mobile");
176
+ });
177
+
178
+ it("does not report the Android stock browser as Safari", () => {
179
+ stubNavigator({ userAgent: UA.androidStock });
180
+ const el = mount();
181
+ expect(el.browserName).toBe(null);
182
+ expect(el.hasAttribute("browser-name")).toBe(false);
183
+ expect(el.browserVersion).toBe(null);
184
+ expect(el.browserEngine).toBe("webkit");
185
+ expect(el.operatingSystem).toBe("android");
186
+ expect(el.deviceType).toBe("mobile");
187
+ });
188
+
189
+ it("detects Chrome on Android as Blink mobile", () => {
190
+ stubNavigator({ userAgent: UA.chromeAndroid, platform: "Linux armv8l" });
191
+ const el = mount();
192
+ expect(el.browserName).toBe("chrome");
193
+ expect(el.browserVersion).toBe("120.0.6099.144");
194
+ expect(el.browserEngine).toBe("blink");
195
+ expect(el.operatingSystem).toBe("android");
196
+ expect(el.deviceType).toBe("mobile");
197
+ });
198
+
199
+ it("detects Chrome for iOS (CriOS)", () => {
200
+ stubNavigator({ userAgent: UA.chromeIos });
201
+ const el = mount();
202
+ expect(el.browserName).toBe("chrome");
203
+ expect(el.browserVersion).toBe("120.0.6099.119");
204
+ expect(el.operatingSystem).toBe("ios");
205
+ });
206
+
207
+ it("detects Chrome on Linux", () => {
208
+ stubNavigator({ userAgent: UA.chromeLinux, platform: "Linux x86_64" });
209
+ const el = mount();
210
+ expect(el.browserName).toBe("chrome");
211
+ expect(el.operatingSystem).toBe("linux");
212
+ expect(el.deviceType).toBe("desktop");
213
+ });
214
+
215
+ it("detects legacy (Presto) Opera on Windows 8.1 with no known engine", () => {
216
+ stubNavigator({ userAgent: UA.operaPresto, vendor: undefined });
217
+ const el = mount();
218
+ expect(el.browserName).toBe("opera");
219
+ expect(el.browserVersion).toBe("9.80");
220
+ expect(el.browserEngine).toBe(null);
221
+ expect(el.hasAttribute("browser-engine")).toBe(false);
222
+ expect(el.operatingSystem).toBe("windows 8.1");
223
+ expect(el.browserVendor).toBe(null);
224
+ });
225
+
226
+ it("detects Internet Explorer 11 (Trident, rv:) on Windows 8", () => {
227
+ stubNavigator({ userAgent: UA.ie11Win8 });
228
+ const el = mount();
229
+ expect(el.browserName).toBe("internet explorer");
230
+ expect(el.browserVersion).toBe("11.0");
231
+ expect(el.browserEngine).toBe("trident");
232
+ expect(el.operatingSystem).toBe("windows 8");
233
+ });
234
+
235
+ it("detects Internet Explorer 10 (MSIE) on Windows 7", () => {
236
+ stubNavigator({ userAgent: UA.ie10Win7 });
237
+ const el = mount();
238
+ expect(el.browserName).toBe("internet explorer");
239
+ expect(el.browserVersion).toBe("10.0");
240
+ expect(el.browserEngine).toBe("trident");
241
+ expect(el.operatingSystem).toBe("windows 7");
242
+ });
243
+
244
+ it("prefers WebKit/Blink over a stray Gecko/ token", () => {
245
+ stubNavigator({ userAgent: UA.geckoAndWebkit });
246
+ const el = mount();
247
+ expect(el.browserEngine).toBe("blink");
248
+ });
249
+ });
250
+
251
+ describe("platform / standalone / language", () => {
252
+ it("falls back to navigator.platform when userAgentData is absent", () => {
253
+ stubNavigator({ userAgentData: undefined, platform: "Win32" });
254
+ const el = mount();
255
+ expect(el.operatingPlatform).toBe("win32");
256
+ });
257
+
258
+ it("reports standalone from the display-mode media query", () => {
259
+ vi.spyOn(window, "matchMedia").mockReturnValue({
260
+ matches: true,
261
+ } as MediaQueryList);
262
+ stubNavigator();
263
+ const el = mount();
264
+ expect(window.matchMedia).toHaveBeenCalledWith(
265
+ "(display-mode: standalone)",
266
+ );
267
+ expect(el.hasAttribute("is-standalone")).toBe(true);
268
+ expect(el.provision?.isStandalone).toBe(true);
269
+ });
270
+
271
+ it("reports standalone from navigator.standalone (Safari)", () => {
272
+ vi.spyOn(window, "matchMedia").mockReturnValue({
273
+ matches: false,
274
+ } as MediaQueryList);
275
+ stubNavigator({ standalone: true });
276
+ const el = mount();
277
+ expect(el.hasAttribute("is-standalone")).toBe(true);
278
+ });
279
+
280
+ it("does not report standalone in a browser tab", () => {
281
+ vi.spyOn(window, "matchMedia").mockReturnValue({
282
+ matches: false,
283
+ } as MediaQueryList);
284
+ stubNavigator({ standalone: false });
285
+ const el = mount();
286
+ expect(el.hasAttribute("is-standalone")).toBe(false);
287
+ expect(el.provision?.isStandalone).toBe(null);
288
+ });
289
+
290
+ it("lowercases the language and exposes the language list", () => {
291
+ stubNavigator({ language: "es-MX", languages: ["es-MX", "es", "en"] });
292
+ const el = mount();
293
+ expect(el.languageId).toBe("es-mx");
294
+ expect(el.provision?.languageIds).toEqual(["es-MX", "es", "en"]);
295
+ });
296
+
297
+ it("reflects cookies-enabled and do-not-track from navigator", () => {
298
+ stubNavigator({ cookieEnabled: false, doNotTrack: "unspecified" });
299
+ const el = mount();
300
+ expect(el.hasAttribute("cookies-enabled")).toBe(false);
301
+ expect(el.provision?.cookiesEnabled).toBe(false);
302
+ expect(el.doNotTrack).toBe("unspecified");
303
+ });
304
+ });
305
+
306
+ describe("sparse navigators", () => {
307
+ it("reports null for everything the navigator does not expose", () => {
308
+ vi.spyOn(window, "matchMedia").mockReturnValue({
309
+ matches: false,
310
+ } as MediaQueryList);
311
+ vi.stubGlobal("navigator", { userAgent: "" });
312
+
313
+ const el = mount();
314
+
315
+ expect(el).dom.to.equalTag(
316
+ `<detect-browser device-type="desktop"></detect-browser>`,
317
+ );
318
+ expect(el.provision).toEqual({
319
+ browserName: null,
320
+ browserVersion: null,
321
+ browserEngine: null,
322
+ browserVendor: null,
323
+ operatingSystem: null,
324
+ operatingPlatform: null,
325
+ deviceType: "desktop",
326
+ isStandalone: null,
327
+ languageId: null,
328
+ cookiesEnabled: null,
329
+ doNotTrack: null,
330
+ userAgent: null,
331
+ languageIds: null,
332
+ hardwareConcurrency: null,
333
+ deviceMemory: null,
334
+ });
335
+ });
336
+
337
+ it("treats a zero hardwareConcurrency as unknown", () => {
338
+ stubNavigator({ hardwareConcurrency: 0, deviceMemory: 0 });
339
+ const el = mount();
340
+ expect(el.provision?.hardwareConcurrency).toBe(null);
341
+ // deviceMemory is only null when undefined: 0 is a real value
342
+ expect(el.provision?.deviceMemory).toBe(0);
343
+ });
344
+ });
345
+
346
+ describe("provision", () => {
347
+ it("publishes provision with neutron-provision on connect", () => {
348
+ stubNavigator();
349
+ const wrap = document.createElement("div");
350
+ document.body.append(wrap);
351
+ const spy = vi.fn();
352
+ wrap.addEventListener("neutron-provision", spy);
353
+
354
+ wrap.innerHTML = `<detect-browser></detect-browser>`;
355
+ const el = wrap.firstElementChild as HTMLDetectBrowserElement;
356
+
357
+ expect(spy).toHaveBeenCalledTimes(1);
358
+ expect(spy.mock.calls[0][0].target).toBe(el);
359
+ expect(el.provision?.browserName).toBe("chrome");
360
+ });
361
+
362
+ it("mirrors every attribute field into provision", () => {
363
+ stubNavigator({ userAgent: UA.edgeWin10 });
364
+ const el = mount();
365
+ expect(el.provision).toEqual(
366
+ expect.objectContaining({
367
+ browserName: "edge",
368
+ browserVersion: "120.0.2210.91",
369
+ browserEngine: "blink",
370
+ browserVendor: "google inc.",
371
+ operatingSystem: "windows 10+",
372
+ operatingPlatform: "macos",
373
+ deviceType: "desktop",
374
+ languageId: "en-us",
375
+ cookiesEnabled: true,
376
+ doNotTrack: "1",
377
+ userAgent: UA.edgeWin10,
378
+ }),
379
+ );
380
+ });
381
+ });
382
+
383
+ describe("reconnection", () => {
384
+ it("does not re-detect when reconnected after a previous mount", async () => {
385
+ stubNavigator({ userAgent: UA.chromeMac });
386
+ const el = mount();
387
+ expect(el.browserName).toBe("chrome");
388
+
389
+ el.remove();
390
+ await wait(0);
391
+
392
+ // a different navigator now, a fresh detection would say firefox
393
+ stubNavigator({ userAgent: UA.firefoxWin7 });
394
+ document.body.append(el);
395
+ await wait(0);
396
+
397
+ expect(el.browserName).toBe("chrome");
398
+ expect(el.operatingSystem).toBe("macos 10.15");
399
+ });
400
+
401
+ it("keeps its attributes when moved synchronously within the document", () => {
402
+ stubNavigator({ userAgent: UA.chromeMac });
403
+ const el = mount();
404
+ const parent = el.parentElement!;
405
+
406
+ stubNavigator({ userAgent: UA.firefoxWin7 });
407
+ el.remove();
408
+ parent.append(el);
409
+
410
+ expect(el.browserName).toBe("chrome");
411
+ });
412
+ });
413
+ });
@@ -0,0 +1,21 @@
1
+ import "../../index";
2
+ import {
3
+ afterEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
8
+ import { mountView, readDemo } from "@excom/quark/support/tests/view-helpers";
9
+
10
+ describe("language view", () => {
11
+ afterEach(() => {
12
+ document.body.innerHTML = "";
13
+ });
14
+
15
+ it("exposes a language-id", async () => {
16
+ const { root } = await mountView(readDemo(import.meta.url, "language"));
17
+ expect(root.querySelector("detect-browser")?.hasAttribute("language-id")).toBe(
18
+ true,
19
+ );
20
+ });
21
+ });
@@ -0,0 +1,29 @@
1
+ import "@excom/quark-sheet";
2
+ import "../../index";
3
+ import {
4
+ afterEach,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ it,
9
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
10
+ import {
11
+ installDemoModules,
12
+ mountView,
13
+ readDemo,
14
+ restoreDemoModules,
15
+ } from "@excom/quark/support/tests/view-helpers";
16
+
17
+ describe("safari view", () => {
18
+ beforeEach(() => installDemoModules());
19
+ afterEach(() => {
20
+ document.body.innerHTML = "";
21
+ restoreDemoModules();
22
+ });
23
+
24
+ it("mounts detect-browser without treating happy-dom as Safari", async () => {
25
+ const { root } = await mountView(readDemo(import.meta.url, "safari"));
26
+ const el = root.querySelector("detect-browser")!;
27
+ expect(el.getAttribute("browser-name")).not.toBe("safari");
28
+ });
29
+ });
@@ -0,0 +1,20 @@
1
+ import "../../index";
2
+ import {
3
+ afterEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
8
+ import { mountView, readDemo } from "@excom/quark/support/tests/view-helpers";
9
+
10
+ describe("simple view", () => {
11
+ afterEach(() => {
12
+ document.body.innerHTML = "";
13
+ });
14
+
15
+ it("populates browser attributes", async () => {
16
+ const { root } = await mountView(readDemo(import.meta.url, "simple"));
17
+ const el = root.querySelector("detect-browser")!;
18
+ expect(el.getAttribute("browser-name") || el.getAttribute("device-type")).toBeTruthy();
19
+ });
20
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "@excom/heft-rig/profiles/default/config/tsconfig.json",
3
+ "include": ["./*.ts"],
4
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
5
+ }