@schukai/monster 4.153.0 → 4.154.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 (52) hide show
  1. package/package.json +1 -1
  2. package/source/components/form/message-state-button.mjs +1 -1
  3. package/source/components/form/select-link.mjs +12 -1
  4. package/source/components/state/log.mjs +3 -7
  5. package/source/components/state/thread.mjs +3 -7
  6. package/source/components/time/day.mjs +0 -19
  7. package/source/components/time/month-calendar.mjs +5 -1
  8. package/source/data/buildmap.mjs +6 -5
  9. package/source/data/buildtree.mjs +0 -8
  10. package/source/data/datasource.mjs +3 -10
  11. package/source/data/diff.mjs +13 -12
  12. package/source/data/extend.mjs +23 -1
  13. package/source/data/pathfinder.mjs +228 -32
  14. package/source/data/transformer.mjs +2 -2
  15. package/source/dom/customcontrol.mjs +35 -9
  16. package/source/dom/customelement.mjs +293 -31
  17. package/source/dom/slotted.mjs +46 -31
  18. package/source/dom/template.mjs +4 -1
  19. package/source/dom/updater.mjs +57 -17
  20. package/source/types/base.mjs +7 -6
  21. package/source/types/basewithoptions.mjs +1 -8
  22. package/source/types/internal.mjs +4 -10
  23. package/source/types/node.mjs +1 -3
  24. package/source/types/observerlist.mjs +9 -17
  25. package/source/types/proxyobserver.mjs +51 -12
  26. package/source/types/tokenlist.mjs +13 -8
  27. package/source/util/clone.mjs +44 -14
  28. package/test/cases/browser-bundle.mjs +75 -0
  29. package/test/cases/components/time/month-calendar.mjs +115 -0
  30. package/test/cases/data/buildmap.mjs +14 -0
  31. package/test/cases/data/buildtree.mjs +49 -0
  32. package/test/cases/data/diff.mjs +86 -0
  33. package/test/cases/data/extend.mjs +52 -0
  34. package/test/cases/data/pathfinder-resolution-baseline.mjs +322 -0
  35. package/test/cases/data/pathfinder.mjs +163 -0
  36. package/test/cases/data/transformer.mjs +26 -0
  37. package/test/cases/dom/customcontrol.mjs +85 -0
  38. package/test/cases/dom/customelement-rebind-baseline.mjs +323 -0
  39. package/test/cases/dom/customelement-teardown.mjs +408 -0
  40. package/test/cases/dom/customelement.mjs +408 -0
  41. package/test/cases/dom/dimension.mjs +34 -10
  42. package/test/cases/dom/slotted-nodes.mjs +48 -1
  43. package/test/cases/dom/template.mjs +11 -0
  44. package/test/cases/dom/updater-remove.mjs +198 -0
  45. package/test/cases/dom/updater.mjs +118 -0
  46. package/test/cases/packaging.mjs +213 -0
  47. package/test/cases/types/base.mjs +50 -0
  48. package/test/cases/types/observerlist.mjs +103 -1
  49. package/test/cases/types/proxyobserver.mjs +189 -0
  50. package/test/cases/types/regex.mjs +4 -0
  51. package/test/cases/types/tokenlist.mjs +64 -1
  52. package/test/cases/util/clone.mjs +115 -0
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+
3
+ import * as chai from "chai";
4
+ import { getDocument } from "../../../source/dom/util.mjs";
5
+ import { chaiDom } from "../../util/chai-dom.mjs";
6
+ import { initJSDOM } from "../../util/jsdom.mjs";
7
+
8
+ let expect = chai.expect;
9
+ chai.use(chaiDom);
10
+
11
+ /**
12
+ * Regressions for issue #579.
13
+ *
14
+ * data-monster-remove had no coverage anywhere in the repository, and its
15
+ * selector relied on a :scope prefix that carries no meaning for a descendant
16
+ * query but does not resolve for shadow root content under jsdom.
17
+ */
18
+ describe("Updater remove attribute issue #579", function () {
19
+ let Updater;
20
+ let CustomElement;
21
+ let registerCustomElement;
22
+ let document;
23
+
24
+ const html = `
25
+ <div id="test1"></div>
26
+ `;
27
+
28
+ before(function (done) {
29
+ initJSDOM({})
30
+ .then(() => import("../../../source/dom/updater.mjs"))
31
+ .then((updaterModule) => {
32
+ Updater = updaterModule["Updater"];
33
+ return import("../../../source/dom/customelement.mjs");
34
+ })
35
+ .then((m) => {
36
+ try {
37
+ CustomElement = m["CustomElement"];
38
+ registerCustomElement = m["registerCustomElement"];
39
+ document = getDocument();
40
+ done();
41
+ } catch (e) {
42
+ done(e);
43
+ }
44
+ })
45
+ .catch(done);
46
+ });
47
+
48
+ beforeEach(function () {
49
+ document.getElementById("mocks").innerHTML = html;
50
+ });
51
+
52
+ afterEach(function () {
53
+ document.getElementById("mocks").innerHTML = "";
54
+ });
55
+
56
+ function settle(ms = 30) {
57
+ return new Promise((resolve) => setTimeout(resolve, ms));
58
+ }
59
+
60
+ describe("light DOM", function () {
61
+ // net: this path already worked, because :scope resolves outside a shadow
62
+ // root, and it must keep working
63
+ it("should remove annotated nodes and keep the rest", async function () {
64
+ const container = document.getElementById("test1");
65
+ container.innerHTML = `
66
+ <div id="keep-me"></div>
67
+ <div id="drop-me" data-monster-remove></div>
68
+ <span data-monster-replace="path:label"></span>
69
+ `;
70
+
71
+ const updater = new Updater(container, { label: "rendered" });
72
+ await updater.run();
73
+ await settle();
74
+
75
+ expect(container.querySelector("#drop-me")).to.equal(null);
76
+ expect(container.querySelector("#keep-me")).to.not.equal(null);
77
+ expect(container.querySelector("span").textContent).to.equal("rendered");
78
+ });
79
+
80
+ it("should remove every annotated node, nested ones included", async function () {
81
+ const container = document.getElementById("test1");
82
+ container.innerHTML = `
83
+ <div id="outer" data-monster-remove>
84
+ <div id="inner" data-monster-remove></div>
85
+ </div>
86
+ <div id="sibling"><div id="deep" data-monster-remove></div></div>
87
+ <span data-monster-replace="path:label"></span>
88
+ `;
89
+
90
+ const updater = new Updater(container, { label: "rendered" });
91
+ await updater.run();
92
+ await settle();
93
+
94
+ expect(container.querySelector("#outer")).to.equal(null);
95
+ expect(container.querySelector("#inner")).to.equal(null);
96
+ expect(container.querySelector("#deep")).to.equal(null);
97
+ expect(container.querySelector("#sibling")).to.not.equal(null);
98
+ });
99
+ });
100
+
101
+ describe("shadow DOM", function () {
102
+ const TAG = "monster-updater-remove-579";
103
+
104
+ before(function () {
105
+ if (!customElements.get(TAG)) {
106
+ class RemoveComponent extends CustomElement {
107
+ static getTag() {
108
+ return TAG;
109
+ }
110
+
111
+ get defaults() {
112
+ return Object.assign({}, super.defaults, {
113
+ label: "rendered",
114
+ templates: {
115
+ main: `
116
+ <div id="host">
117
+ <div id="keep-me"></div>
118
+ <div id="drop-me" data-monster-remove></div>
119
+ <span id="label" data-monster-replace="path:label"></span>
120
+ </div>
121
+ `,
122
+ },
123
+ });
124
+ }
125
+ }
126
+
127
+ registerCustomElement(RemoveComponent);
128
+ }
129
+ });
130
+
131
+ // regression: the annotated node survived inside a shadow root
132
+ it("should remove annotated nodes from a component shadow root", async function () {
133
+ const element = document.createElement(TAG);
134
+ document.getElementById("test1").appendChild(element);
135
+ await settle();
136
+
137
+ expect(element.shadowRoot.querySelector("#drop-me")).to.equal(null);
138
+ expect(element.shadowRoot.querySelector("#keep-me")).to.not.equal(null);
139
+ expect(element.shadowRoot.querySelector("#label").textContent).to.equal(
140
+ "rendered",
141
+ );
142
+ expect(element.hasAttribute("data-monster-error")).to.be.false;
143
+ });
144
+
145
+ it("should keep the shadow root free of annotated nodes after an update", async function () {
146
+ const element = document.createElement(TAG);
147
+ document.getElementById("test1").appendChild(element);
148
+ await settle();
149
+
150
+ element.setOption("label", "changed");
151
+ await settle();
152
+
153
+ expect(element.shadowRoot.querySelector("#label").textContent).to.equal(
154
+ "changed",
155
+ );
156
+ expect(
157
+ element.shadowRoot.querySelectorAll("[data-monster-remove]").length,
158
+ ).to.equal(0);
159
+ });
160
+
161
+ it("should remove an annotated node nested below the updater element", async function () {
162
+ const nestedTag = "monster-updater-remove-nested-579";
163
+
164
+ if (!customElements.get(nestedTag)) {
165
+ class NestedRemoveComponent extends CustomElement {
166
+ static getTag() {
167
+ return nestedTag;
168
+ }
169
+
170
+ get defaults() {
171
+ return Object.assign({}, super.defaults, {
172
+ label: "rendered",
173
+ templates: {
174
+ main: `
175
+ <div id="level-one">
176
+ <div id="level-two">
177
+ <em id="deep-drop" data-monster-remove></em>
178
+ <b id="deep-keep"></b>
179
+ </div>
180
+ </div>
181
+ `,
182
+ },
183
+ });
184
+ }
185
+ }
186
+
187
+ registerCustomElement(NestedRemoveComponent);
188
+ }
189
+
190
+ const element = document.createElement(nestedTag);
191
+ document.getElementById("test1").appendChild(element);
192
+ await settle();
193
+
194
+ expect(element.shadowRoot.querySelector("#deep-drop")).to.equal(null);
195
+ expect(element.shadowRoot.querySelector("#deep-keep")).to.not.equal(null);
196
+ });
197
+ });
198
+ });
@@ -1665,6 +1665,78 @@ describe("DOM", function () {
1665
1665
  .catch((e) => done(new Error(e)));
1666
1666
  });
1667
1667
 
1668
+ it("should not move unchanged keyed nodes issue #564", async function () {
1669
+ const element = document.getElementById("test-patch");
1670
+ const a = document.createElement("span");
1671
+ const b = document.createElement("span");
1672
+ a.dataset.key = "a";
1673
+ b.dataset.key = "b";
1674
+
1675
+ const updater = new Updater(element, {
1676
+ text: "",
1677
+ contentNode: document.createElement("div"),
1678
+ fragmentNode: document.createDocumentFragment(),
1679
+ itemsArray: [],
1680
+ keyedItems: [a, b],
1681
+ keyedDataItems: [],
1682
+ items: [],
1683
+ });
1684
+ updater.setCallback("getPatchKey", (value) => value?.dataset?.key);
1685
+ await updater.run();
1686
+
1687
+ const target = document.getElementById("patch-keyed");
1688
+ let moves = 0;
1689
+ const appendChild = target.appendChild;
1690
+ const insertBefore = target.insertBefore;
1691
+ target.appendChild = function (...args) {
1692
+ moves++;
1693
+ return appendChild.apply(this, args);
1694
+ };
1695
+ target.insertBefore = function (...args) {
1696
+ moves++;
1697
+ return insertBefore.apply(this, args);
1698
+ };
1699
+
1700
+ updater.getSubject().keyedItems = [a, b];
1701
+ await new Promise((resolve) => setTimeout(resolve, 30));
1702
+
1703
+ expect(moves).to.equal(0);
1704
+ expect(Array.from(target.children)).to.deep.equal([a, b]);
1705
+ });
1706
+
1707
+ it("should dispose a keyed node replaced under the same key issue #564", async function () {
1708
+ const element = document.getElementById("test-patch");
1709
+ const first = document.createElement("span");
1710
+ const replacement = document.createElement("span");
1711
+ first.dataset.key = "same";
1712
+ replacement.dataset.key = "same";
1713
+ let disposeCount = 0;
1714
+ addToObjectLink(
1715
+ first,
1716
+ customElementUpdaterLinkSymbol,
1717
+ new Set([{dispose() { disposeCount++; }}]),
1718
+ );
1719
+
1720
+ const updater = new Updater(element, {
1721
+ text: "",
1722
+ contentNode: document.createElement("div"),
1723
+ fragmentNode: document.createDocumentFragment(),
1724
+ itemsArray: [],
1725
+ keyedItems: [first],
1726
+ keyedDataItems: [],
1727
+ items: [],
1728
+ });
1729
+ updater.setCallback("getPatchKey", (value) => value?.dataset?.key);
1730
+ await updater.run();
1731
+
1732
+ updater.getSubject().keyedItems = [replacement];
1733
+ await new Promise((resolve) => setTimeout(resolve, 30));
1734
+
1735
+ expect(disposeCount).to.equal(1);
1736
+ expect(first.isConnected).to.equal(false);
1737
+ expect(document.getElementById("patch-keyed").firstChild).to.equal(replacement);
1738
+ });
1739
+
1668
1740
  it("should remove stale keyed nodes and keep surviving node identity", function (done) {
1669
1741
  let element = document.getElementById("test-patch");
1670
1742
  const a = document.createElement("span");
@@ -2620,6 +2692,52 @@ describe("DOM", function () {
2620
2692
  });
2621
2693
  });
2622
2694
 
2695
+ describe("Updater binding correctness issue #563", function () {
2696
+ it("should ignore unchecked radios during retrieve", function () {
2697
+ const mocks = document.getElementById("mocks");
2698
+ mocks.innerHTML = `
2699
+ <input type="radio" value="selected" checked data-monster-bind="path:choice">
2700
+ <input type="radio" value="unchecked" data-monster-bind="path:choice">
2701
+ `;
2702
+
2703
+ const updater = new Updater(mocks, {choice: "initial"});
2704
+ updater.retrieve();
2705
+
2706
+ expect(updater.getSubject().choice).to.equal("selected");
2707
+ });
2708
+
2709
+ it("should not change select state for non-value attributes", async function () {
2710
+ const mocks = document.getElementById("mocks");
2711
+ mocks.innerHTML = `
2712
+ <select id="binding-select" data-monster-attributes="title path:title">
2713
+ <option value="a">a</option>
2714
+ <option value="b" selected>b</option>
2715
+ </select>
2716
+ `;
2717
+
2718
+ const updater = new Updater(mocks, {title: "label"});
2719
+ await updater.run();
2720
+
2721
+ const select = document.getElementById("binding-select");
2722
+ expect(select.title).to.equal("label");
2723
+ expect(select.value).to.equal("b");
2724
+ });
2725
+
2726
+ it("should evaluate static attribute and property commands", async function () {
2727
+ const mocks = document.getElementById("mocks");
2728
+ mocks.innerHTML = `
2729
+ <div id="static-attribute" data-monster-attributes="title static:hello"></div>
2730
+ <input id="static-property" data-monster-properties="value static:world">
2731
+ `;
2732
+
2733
+ const updater = new Updater(mocks, {trigger: true});
2734
+ await updater.run();
2735
+
2736
+ expect(document.getElementById("static-attribute").title).to.equal("hello");
2737
+ expect(document.getElementById("static-property").value).to.equal("world");
2738
+ });
2739
+ });
2740
+
2623
2741
  describe("Updater pipe failure recovery issue #498", function () {
2624
2742
  const waitForUpdate = () => new Promise((resolve) => setTimeout(resolve, 20));
2625
2743
 
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+
3
+ import { expect } from "chai";
4
+ import { existsSync, readFileSync, statSync } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ /**
9
+ * Packaging regressions for issue #577.
10
+ *
11
+ * Two classes of mistake are guarded here. An exports map that does not cover
12
+ * an established import path turns a patch release into a breaking change, and
13
+ * a field added to package.json never reaches the npm package unless the
14
+ * whitelist in nix/scripts/build.nix carries it too, because the published
15
+ * manifest is reassembled there from scratch.
16
+ *
17
+ * This file reads the repository, so it is listed in nodeOnlyCases in
18
+ * test/web/current-bundle.mjs and does not run in the browser bundle.
19
+ */
20
+ const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
21
+
22
+ function readPackageJson() {
23
+ return JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf8"));
24
+ }
25
+
26
+ function readBuildNix() {
27
+ return readFileSync(join(projectRoot, "nix/scripts/build.nix"), "utf8");
28
+ }
29
+
30
+ describe("Packaging issue #577", function () {
31
+ describe("exports map", function () {
32
+ it("should resolve every literal export target to an existing file", function () {
33
+ const pkg = readPackageJson();
34
+ expect(pkg.exports, "exports map is missing").to.be.an("object");
35
+
36
+ for (const [subpath, target] of Object.entries(pkg.exports)) {
37
+ if (typeof target !== "string" || target.includes("*")) {
38
+ continue;
39
+ }
40
+
41
+ const absolute = join(projectRoot, target);
42
+ expect(
43
+ existsSync(absolute),
44
+ `${subpath} points at a missing file: ${target}`,
45
+ ).to.be.true;
46
+ }
47
+ });
48
+
49
+ it("should expose the package entry point", function () {
50
+ const pkg = readPackageJson();
51
+ const entry = pkg.exports["."];
52
+
53
+ expect(entry, "no entry point exported").to.not.equal(undefined);
54
+
55
+ const target = typeof entry === "string" ? entry : entry.default;
56
+ expect(target).to.equal(`./${pkg.main}`);
57
+ expect(existsSync(join(projectRoot, target))).to.be.true;
58
+ });
59
+
60
+ it("should keep every published top level path reachable", function () {
61
+ const pkg = readPackageJson();
62
+ const patterns = Object.keys(pkg.exports);
63
+
64
+ function covered(entry) {
65
+ const head = entry.split("/")[0];
66
+ return patterns.some(
67
+ (pattern) =>
68
+ pattern === `./${entry}` ||
69
+ pattern === `./${head}/*` ||
70
+ pattern.startsWith(`./${head}/`),
71
+ );
72
+ }
73
+
74
+ // Adding an exports map restricts what consumers may import. Anything
75
+ // listed in files was importable before and has to stay importable.
76
+ for (const entry of pkg.files) {
77
+ expect(covered(entry), `files entry "${entry}" is not covered by exports`)
78
+ .to.be.true;
79
+ }
80
+
81
+ // npm publishes these regardless of files, so they were reachable too
82
+ for (const entry of ["README.md", "LICENSE"]) {
83
+ expect(
84
+ covered(entry),
85
+ `"${entry}" ships in the package but is not covered by exports`,
86
+ ).to.be.true;
87
+ }
88
+ });
89
+
90
+ it("should resolve established deep import paths through the patterns", function () {
91
+ const pkg = readPackageJson();
92
+
93
+ const established = [
94
+ "source/monster.mjs",
95
+ "source/dom/customelement.mjs",
96
+ "source/dom/updater.mjs",
97
+ "source/data/pathfinder.mjs",
98
+ "source/types/tokenlist.mjs",
99
+ "source/util/clone.mjs",
100
+ "source/components/form/select.mjs",
101
+ "test/util/jsdom.mjs",
102
+ ];
103
+
104
+ for (const path of established) {
105
+ const head = path.split("/")[0];
106
+ const pattern = pkg.exports[`./${head}/*`];
107
+
108
+ expect(pattern, `no pattern export for ./${head}/*`).to.be.a("string");
109
+
110
+ const target = pattern.replace("*", path.slice(head.length + 1));
111
+ expect(
112
+ existsSync(join(projectRoot, target)),
113
+ `${path} does not resolve through ${pattern}`,
114
+ ).to.be.true;
115
+ }
116
+ });
117
+ });
118
+
119
+ describe("side effect metadata", function () {
120
+ it("should not mark component modules as side effect free", function () {
121
+ const pkg = readPackageJson();
122
+
123
+ expect(pkg.sideEffects, "sideEffects is missing").to.be.an("array");
124
+
125
+ // every component module calls registerCustomElement() at module scope,
126
+ // so a bundler must never drop one
127
+ const covered = pkg.sideEffects.some(
128
+ (pattern) =>
129
+ pattern === "./source/components/**" ||
130
+ pattern === "./source/**" ||
131
+ pattern === "./source/components/*",
132
+ );
133
+
134
+ expect(covered, "component modules are not declared side effectful").to.be
135
+ .true;
136
+ expect(pkg.sideEffects).to.include("./source/monster.mjs");
137
+ });
138
+ });
139
+
140
+ describe("published manifest", function () {
141
+ it("should carry every publish relevant field into the build whitelist", function () {
142
+ const pkg = readPackageJson();
143
+ const buildNix = readBuildNix();
144
+
145
+ // fields that only matter for development stay out of the package
146
+ const developmentOnly = new Set([
147
+ "devDependencies",
148
+ "main",
149
+ "module",
150
+ "type",
151
+ ]);
152
+
153
+ for (const field of Object.keys(pkg)) {
154
+ if (developmentOnly.has(field)) {
155
+ continue;
156
+ }
157
+
158
+ expect(
159
+ buildNix.includes(`${field} =`),
160
+ `package.json field "${field}" is missing from releasedPackageJson in nix/scripts/build.nix, so it never reaches the npm package`,
161
+ ).to.be.true;
162
+ }
163
+ });
164
+ });
165
+
166
+ describe("generated barrel", function () {
167
+ it("should export the core symbols", function () {
168
+ const barrel = readFileSync(
169
+ join(projectRoot, "source/monster.mjs"),
170
+ "utf8",
171
+ );
172
+
173
+ const core = [
174
+ "./dom/customelement.mjs",
175
+ "./dom/customcontrol.mjs",
176
+ "./dom/updater.mjs",
177
+ "./data/pathfinder.mjs",
178
+ "./data/diff.mjs",
179
+ "./data/extend.mjs",
180
+ "./types/base.mjs",
181
+ "./types/proxyobserver.mjs",
182
+ "./types/observer.mjs",
183
+ "./types/tokenlist.mjs",
184
+ "./util/clone.mjs",
185
+ ];
186
+
187
+ for (const module of core) {
188
+ expect(
189
+ barrel.includes(`from "${module}"`),
190
+ `the generated barrel no longer re-exports ${module}`,
191
+ ).to.be.true;
192
+ }
193
+ });
194
+
195
+ it("should reference only existing modules", function () {
196
+ const barrel = readFileSync(
197
+ join(projectRoot, "source/monster.mjs"),
198
+ "utf8",
199
+ );
200
+
201
+ const matches = [...barrel.matchAll(/from "(\.\/[^"]+)"/g)];
202
+ expect(matches.length).to.be.greaterThan(100);
203
+
204
+ for (const match of matches) {
205
+ const target = join(projectRoot, "source", match[1]);
206
+ expect(
207
+ existsSync(target) && statSync(target).isFile(),
208
+ `the barrel references a missing module: ${match[1]}`,
209
+ ).to.be.true;
210
+ }
211
+ });
212
+ });
213
+ });
@@ -25,6 +25,26 @@ class Subclass extends BaseDifferentRealm {
25
25
  }
26
26
  }
27
27
 
28
+ // an own instanceSymbol whose getter yields undefined, which is the only way to
29
+ // reach the traversal branch that lost its receiver, see issue #574
30
+ class UndefinedSymbolBelowBase extends Base {
31
+ static get [instanceSymbol]() {
32
+ return undefined;
33
+ }
34
+ }
35
+
36
+ class UnrelatedWithUndefinedSymbol extends Object {
37
+ static get [instanceSymbol]() {
38
+ return undefined;
39
+ }
40
+ }
41
+
42
+ class DeepUndefinedSymbol extends UndefinedSymbolBelowBase {
43
+ static get [instanceSymbol]() {
44
+ return undefined;
45
+ }
46
+ }
47
+
28
48
  describe('Base', function () {
29
49
 
30
50
  describe('new Base', function () {
@@ -60,4 +80,34 @@ describe('Base', function () {
60
80
 
61
81
  })
62
82
 
83
+ describe('instance symbol traversal issue #574', function () {
84
+
85
+ // net: traversal results that are already correct
86
+ it('should keep reporting a subclass with its own symbol as an instance', function () {
87
+ expect(new Subclass).to.be.instanceOf(Base);
88
+ });
89
+
90
+ it('should not report a plain object or a foreign class as an instance', function () {
91
+ expect({}).to.not.be.instanceOf(Base);
92
+ expect(new (class Foreign {})).to.not.be.instanceOf(Base);
93
+ });
94
+
95
+ // regression: the branch for an undefined symbol dropped its receiver, so
96
+ // `this.hasOwnProperty` threw a TypeError in strict mode module code
97
+ it('should not throw when a class in the chain yields an undefined symbol', function () {
98
+ expect(() => new UndefinedSymbolBelowBase instanceof Base).to.not.throw();
99
+ expect(() => new UnrelatedWithUndefinedSymbol instanceof Base).to.not.throw();
100
+ });
101
+
102
+ it('should keep walking the chain past an undefined symbol', function () {
103
+ expect(new UndefinedSymbolBelowBase).to.be.instanceOf(Base);
104
+ expect(new DeepUndefinedSymbol).to.be.instanceOf(Base);
105
+ });
106
+
107
+ it('should still report false for an unrelated class with an undefined symbol', function () {
108
+ expect(new UnrelatedWithUndefinedSymbol).to.not.be.instanceOf(Base);
109
+ });
110
+
111
+ })
112
+
63
113
  })