Skip to content

Commit 521c8c5

Browse files
author
Anatoly Ostrovsky
committed
Scope and messages optimizations
1 parent a814f74 commit 521c8c5

11 files changed

Lines changed: 1278 additions & 111 deletions

File tree

Makefile

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: build build-ts test types coverage coverage-check coverage-open
1+
.PHONY: build build-ts test types coverage coverage-check coverage-open setup ensure-deps
22

33
BUILD_DIR = ./dist
44
TS_BUILD_DIR = ./.build
@@ -7,29 +7,25 @@ GZ_JS := $(MIN_JS).gz
77

88

99
setup:
10-
@rm -r ./node_modules/
11-
@npm i
10+
@rm -rf ./node_modules/
11+
@npm ci
1212
@npx playwright install
13-
14-
build:
13+
14+
ensure-deps:
15+
@if [ ! -d ./node_modules ]; then \
16+
echo "Installing dependencies..."; \
17+
npm ci; \
18+
fi
19+
20+
build: ensure-deps
1521
@if [ -d "$(BUILD_DIR)" ]; then \
1622
echo "Removing $(BUILD_DIR)..."; \
1723
rm -r "$(BUILD_DIR)"; \
1824
fi
19-
@if [ -d "$(TS_BUILD_DIR)" ]; then \
20-
echo "Removing $(TS_BUILD_DIR)..."; \
21-
rm -r "$(TS_BUILD_DIR)"; \
22-
fi
23-
@npm i
2425
@./node_modules/.bin/tsc --project tsconfig.build.json
2526
@./node_modules/.bin/rollup -c
26-
@rm -rf .build
2727

28-
build-ts:
29-
@if [ -d "$(TS_BUILD_DIR)" ]; then \
30-
echo "Removing $(TS_BUILD_DIR)..."; \
31-
rm -r "$(TS_BUILD_DIR)"; \
32-
fi
28+
build-ts: ensure-deps
3329
@./node_modules/.bin/tsc --project tsconfig.build.json
3430

3531
size:
@@ -62,24 +58,24 @@ format:
6258
lint:
6359
@npx eslint ./src --fix
6460

65-
check:
61+
check: ensure-deps
6662
@echo "Typechecking Js"
6763
./node_modules/.bin/tsc
6864

69-
types:
65+
types: ensure-deps
7066
@echo "Generating *.d.ts"
7167
@rm -rf @types
7268
@./node_modules/.bin/tsc --project tsconfig.types.json
7369
@npx prettier ./@types --write --cache --log-level=silent
7470

7571
TYPEDOC_DIR = docs/static/typedoc
76-
doc:
72+
doc: ensure-deps
7773
@rm -rf $(TYPEDOC_DIR)
7874
@node_modules/.bin/typedoc
7975
@npx prettier ./typedoc --write
8076
@mv typedoc $(TYPEDOC_DIR)
8177

82-
serve:
78+
serve: ensure-deps
8379
@node_modules/.bin/vite --config utils/vite.config.js & \
8480
node --watch ./utils/express.js & \
8581
wait
@@ -88,23 +84,23 @@ prepare-release: build test check types doc format gzip version size-html
8884

8985
PLAYWRIGHT_TEST := npx playwright test
9086

91-
test:
87+
test: ensure-deps
9288
@echo $(INFO) "Playwright test JS"
9389
@$(PLAYWRIGHT_TEST)
9490

95-
test-ui:
91+
test-ui: ensure-deps
9692
@echo $(INFO) "Playwright test JS with ui"
9793
@$(PLAYWRIGHT_TEST) --ui
9894

99-
coverage:
95+
coverage: ensure-deps
10096
@echo $(INFO) "Playwright coverage"
10197
@node ./utils/run-coverage.mjs
10298

103-
coverage-check:
99+
coverage-check: ensure-deps
104100
@echo $(INFO) "Playwright coverage threshold check"
105101
@node ./utils/run-coverage.mjs --check
106102

107-
coverage-open:
103+
coverage-open: ensure-deps
108104
@echo $(INFO) "Open coverage report"
109105
@node ./utils/open-coverage.mjs
110106

playwright.config.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,31 @@ import { defineConfig, devices } from "@playwright/test";
66
*/
77
// require('dotenv').config();
88
const baseUrl = "http://localhost:4000";
9+
const isCI =
10+
"process" in globalThis &&
11+
!!(
12+
globalThis as typeof globalThis & {
13+
process?: { env?: Record<string, string | undefined> };
14+
}
15+
).process?.env?.CI;
916

1017
/**
1118
* See https://playwright.dev/docs/test-configuration.
1219
*/
1320
export default defineConfig({
14-
testDir: "./",
21+
testDir: "./src",
1522
testMatch: "**/*.test.js",
1623
testIgnore: ["docs/public/**"],
1724
/* Run tests in files in parallel */
1825
fullyParallel: false,
1926
/* Fail the build on CI if you accidentally left test.only in the source code. */
20-
forbidOnly: !!process.env.CI,
27+
forbidOnly: isCI,
2128
/* Retry on CI only */
22-
retries: process.env.CI ? 2 : 0,
23-
/* Opt out of parallel tests on CI. */
29+
retries: isCI ? 2 : 0,
30+
/* Keep the browser/Jasmine suite serialized because test files share page state. */
2431
workers: 1,
2532
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
26-
reporter: "html",
33+
reporter: isCI ? "html" : "line",
2734
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
2835
use: {
2936
/* Base URL to use in actions like `await page.goto('/')`. */

src/binding.spec.js

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,30 @@ describe("binding", () => {
246246
elem.remove();
247247
});
248248

249+
it("should still refresh tuple key bindings when the first observed array item is removed", async () => {
250+
let elem = createElementFromHTML(
251+
"<ul>" +
252+
'<li ng-repeat="(key, item) in items"><span class="index" ng-bind="key"></span>:<span class="label" ng-bind="item.a"></span></li>' +
253+
"</ul>",
254+
);
255+
document.getElementById("app").insertAdjacentElement("afterend", elem);
256+
$injector = window.angular.bootstrap(elem, ["myModule"]);
257+
$rootScope = $injector.get("$rootScope");
258+
259+
$rootScope.items = [{ a: "A" }, { a: "B" }, { a: "C" }];
260+
await wait();
261+
262+
$rootScope.items.splice(0, 1);
263+
await wait();
264+
265+
const rows = Array.from(elem.querySelectorAll("li")).map(
266+
(row) => row.textContent,
267+
);
268+
269+
expect(rows).toEqual(["0:B", "1:C"]);
270+
elem.remove();
271+
});
272+
249273
it("should preserve surviving repeated DOM nodes when leading observed array items are removed in bulk", async () => {
250274
let elem = createElementFromHTML(
251275
"<ul>" +
@@ -283,6 +307,152 @@ describe("binding", () => {
283307
elem.remove();
284308
});
285309

310+
it("should preserve existing repeated DOM nodes when observed array items are appended via array replacement", async () => {
311+
let elem = createElementFromHTML(
312+
"<ul>" +
313+
'<li ng-repeat="item in items" ng-bind="item.a"></li>' +
314+
"</ul>",
315+
);
316+
document.getElementById("app").insertAdjacentElement("afterend", elem);
317+
$injector = window.angular.bootstrap(elem, ["myModule"]);
318+
$rootScope = $injector.get("$rootScope");
319+
320+
$rootScope.items = [{ a: "A" }, { a: "B" }];
321+
await wait();
322+
323+
const originalRows = elem.querySelectorAll("li");
324+
const firstRow = originalRows[0];
325+
const secondRow = originalRows[1];
326+
327+
firstRow.setAttribute("data-row", "survivor-a");
328+
secondRow.setAttribute("data-row", "survivor-b");
329+
330+
$rootScope.items = $rootScope.items.concat([{ a: "C" }, { a: "D" }]);
331+
await wait();
332+
333+
const currentRows = elem.querySelectorAll("li");
334+
335+
expect(currentRows.length).toBe(4);
336+
expect(currentRows[0]).toBe(firstRow);
337+
expect(currentRows[1]).toBe(secondRow);
338+
expect(elem.outerHTML).toBe(
339+
"<ul><!---->" +
340+
'<li ng-repeat="item in items" ng-bind="item.a" data-row="survivor-a">A</li>' +
341+
'<li ng-repeat="item in items" ng-bind="item.a" data-row="survivor-b">B</li>' +
342+
'<li ng-repeat="item in items" ng-bind="item.a">C</li>' +
343+
'<li ng-repeat="item in items" ng-bind="item.a">D</li>' +
344+
"</ul>",
345+
);
346+
elem.remove();
347+
});
348+
349+
it("should preserve existing repeated DOM nodes when observed array items are appended via push", async () => {
350+
let elem = createElementFromHTML(
351+
"<ul>" +
352+
'<li ng-repeat="item in items" ng-bind="item.a"></li>' +
353+
"</ul>",
354+
);
355+
document.getElementById("app").insertAdjacentElement("afterend", elem);
356+
$injector = window.angular.bootstrap(elem, ["myModule"]);
357+
$rootScope = $injector.get("$rootScope");
358+
359+
$rootScope.items = [{ a: "A" }, { a: "B" }];
360+
await wait();
361+
362+
const originalRows = elem.querySelectorAll("li");
363+
const firstRow = originalRows[0];
364+
const secondRow = originalRows[1];
365+
366+
firstRow.setAttribute("data-row", "survivor-a");
367+
secondRow.setAttribute("data-row", "survivor-b");
368+
369+
$rootScope.items.push({ a: "C" }, { a: "D" });
370+
await wait();
371+
372+
const currentRows = elem.querySelectorAll("li");
373+
374+
expect(currentRows.length).toBe(4);
375+
expect(currentRows[0]).toBe(firstRow);
376+
expect(currentRows[1]).toBe(secondRow);
377+
expect(elem.outerHTML).toBe(
378+
"<ul><!---->" +
379+
'<li ng-repeat="item in items" ng-bind="item.a" data-row="survivor-a">A</li>' +
380+
'<li ng-repeat="item in items" ng-bind="item.a" data-row="survivor-b">B</li>' +
381+
'<li ng-repeat="item in items" ng-bind="item.a">C</li>' +
382+
'<li ng-repeat="item in items" ng-bind="item.a">D</li>' +
383+
"</ul>",
384+
);
385+
elem.remove();
386+
});
387+
388+
it("should preserve repeated DOM nodes when observed array items are swapped by index", async () => {
389+
let elem = createElementFromHTML(
390+
"<ul>" +
391+
'<li ng-repeat="item in items" ng-bind="item.a"></li>' +
392+
"</ul>",
393+
);
394+
document.getElementById("app").insertAdjacentElement("afterend", elem);
395+
$injector = window.angular.bootstrap(elem, ["myModule"]);
396+
$rootScope = $injector.get("$rootScope");
397+
398+
$rootScope.items = [{ a: "A" }, { a: "B" }, { a: "C" }, { a: "D" }];
399+
await wait();
400+
401+
const originalRows = elem.querySelectorAll("li");
402+
const secondRow = originalRows[1];
403+
const fourthRow = originalRows[3];
404+
405+
secondRow.setAttribute("data-row", "survivor-b");
406+
fourthRow.setAttribute("data-row", "survivor-d");
407+
408+
const tmp = $rootScope.items[1];
409+
410+
$rootScope.items[1] = $rootScope.items[3];
411+
$rootScope.items[3] = tmp;
412+
await wait();
413+
414+
const currentRows = elem.querySelectorAll("li");
415+
416+
expect(currentRows[1]).toBe(fourthRow);
417+
expect(currentRows[3]).toBe(secondRow);
418+
expect(elem.outerHTML).toBe(
419+
"<ul><!---->" +
420+
'<li ng-repeat="item in items" ng-bind="item.a">A</li>' +
421+
'<li ng-repeat="item in items" ng-bind="item.a" data-row="survivor-d">D</li>' +
422+
'<li ng-repeat="item in items" ng-bind="item.a">C</li>' +
423+
'<li ng-repeat="item in items" ng-bind="item.a" data-row="survivor-b">B</li>' +
424+
"</ul>",
425+
);
426+
elem.remove();
427+
});
428+
429+
it("should still refresh $index bindings when observed array items are swapped by index", async () => {
430+
let elem = createElementFromHTML(
431+
"<ul>" +
432+
'<li ng-repeat="item in items"><span class="index" ng-bind="$index"></span>:<span class="label" ng-bind="item.a"></span></li>' +
433+
"</ul>",
434+
);
435+
document.getElementById("app").insertAdjacentElement("afterend", elem);
436+
$injector = window.angular.bootstrap(elem, ["myModule"]);
437+
$rootScope = $injector.get("$rootScope");
438+
439+
$rootScope.items = [{ a: "A" }, { a: "B" }, { a: "C" }];
440+
await wait();
441+
442+
const tmp = $rootScope.items[0];
443+
444+
$rootScope.items[0] = $rootScope.items[2];
445+
$rootScope.items[2] = tmp;
446+
await wait();
447+
448+
const rows = Array.from(elem.querySelectorAll("li")).map(
449+
(row) => row.textContent,
450+
);
451+
452+
expect(rows).toEqual(["0:C", "1:B", "2:A"]);
453+
elem.remove();
454+
});
455+
286456
it("should still refresh $index bindings when leading observed array items are removed in bulk", async () => {
287457
let elem = createElementFromHTML(
288458
"<ul>" +
@@ -368,6 +538,47 @@ describe("binding", () => {
368538
elem.remove();
369539
});
370540

541+
it("should preserve surviving repeated DOM nodes when observed array items are removed from the tail", async () => {
542+
let elem = createElementFromHTML(
543+
"<ul>" +
544+
'<li ng-repeat="item in items" ng-bind="item.a"></li>' +
545+
"</ul>",
546+
);
547+
document.getElementById("app").insertAdjacentElement("afterend", elem);
548+
$injector = window.angular.bootstrap(elem, ["myModule"]);
549+
$rootScope = $injector.get("$rootScope");
550+
551+
$rootScope.items = [{ a: "A" }, { a: "B" }, { a: "C" }, { a: "D" }];
552+
await wait();
553+
554+
const originalRows = elem.querySelectorAll("li");
555+
const survivingFirstRow = originalRows[0];
556+
const survivingSecondRow = originalRows[1];
557+
const survivingThirdRow = originalRows[2];
558+
559+
survivingFirstRow.setAttribute("data-row", "survivor-a");
560+
survivingSecondRow.setAttribute("data-row", "survivor-b");
561+
survivingThirdRow.setAttribute("data-row", "survivor-c");
562+
563+
$rootScope.items.pop();
564+
await wait();
565+
566+
const currentRows = elem.querySelectorAll("li");
567+
568+
expect(currentRows.length).toBe(3);
569+
expect(currentRows[0]).toBe(survivingFirstRow);
570+
expect(currentRows[1]).toBe(survivingSecondRow);
571+
expect(currentRows[2]).toBe(survivingThirdRow);
572+
expect(elem.outerHTML).toBe(
573+
"<ul><!---->" +
574+
'<li ng-repeat="item in items" ng-bind="item.a" data-row="survivor-a">A</li>' +
575+
'<li ng-repeat="item in items" ng-bind="item.a" data-row="survivor-b">B</li>' +
576+
'<li ng-repeat="item in items" ng-bind="item.a" data-row="survivor-c">C</li>' +
577+
"</ul>",
578+
);
579+
elem.remove();
580+
});
581+
371582
it("should clear repeated DOM when an observed array is truncated via length assignment", async () => {
372583
let elem = createElementFromHTML(
373584
"<ul>" +

0 commit comments

Comments
 (0)