Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,18 @@
"@typescript-eslint/no-this-alias": "warn",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-extra-semi": "error",
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"@typescript-eslint/no-wrapper-object-types": "off",
"@typescript-eslint/no-unsafe-declaration-merging": "off",
"@typescript-eslint/no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true }],
"no-extra-semi": "off"
}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"],
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
}
},
Expand Down
9 changes: 1 addition & 8 deletions packages/core/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
{
"extends": "../../.eslintrc.json",
"rules": {},
"ignorePatterns": ["!**/*", "**/global-types.d.ts", "**/node_modules/**/*", "**/__tests__/**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"],
"ignorePatterns": ["!**/*", "**/global-types.d.ts", "**/node_modules/**/*", "**/__tests__/**/*", "**/platforms/**/*", "**/css/lib/**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["packages/core/tsconfig.*?.json"]
},
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
Expand Down
108 changes: 108 additions & 0 deletions packages/core/__tests__/benchmarks/css-selector.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { bench, describe } from 'vitest';
import { parse } from '../../css/reworkcss.js';
import { AttributeSelector, createSelector, RuleSet, StyleSheetSelectorScope } from '../../ui/styling/css-selector';
import { _populateRules } from '../../ui/styling/style-scope';

function createScope(css: string): StyleSheetSelectorScope<any> {
const parsed = parse(css, { source: 'css-selector.bench.ts' });
const rulesets: RuleSet[] = [];
_populateRules(parsed.stylesheet.rules, rulesets, []);

return new StyleSheetSelectorScope(rulesets);
}

// Build a stylesheet shaped like a typical themed app (theme-core ships
// thousands of selectors): universal rules, sizable per-type buckets, many
// class-variant buckets, ids and a couple of media queries.
function buildStylesheet(): string {
const parts: string[] = ['* { font-family: sans-serif; }', '* { font-size: 14; }', 'view { color: black; }'];

const types = ['button', 'label', 'stacklayout', 'gridlayout', 'image', 'textfield', 'listview', 'scrollview'];
for (const type of types) {
for (let i = 0; i < 12; i++) {
parts.push(`${type}.variant-${i} { color: red; margin: ${i}; }`);
}
parts.push(`${type} { color: red; }`);
parts.push(`${type}.accent { color: blue; }`);
}

for (let i = 0; i < 60; i++) {
for (let j = 0; j < 6; j++) {
parts.push(`.cls-${i}.mod-${j} { margin: ${i + j}; }`);
}
parts.push(`.cls-${i} { margin: ${i}; }`);
parts.push(`.cls-${i} .child-${i} { padding: ${i}; }`);
}

for (let i = 0; i < 10; i++) {
parts.push(`#id-${i} { color: green; }`);
}

parts.push('@media only screen and (min-width: 400) { button { color: purple; } .cls-1 { color: orange; } }');
parts.push('@media only screen and (min-width: 400) and (max-width: 5000) { label { color: yellow; } }');

return parts.join('\n');
}

const scope = createScope(buildStylesheet());

const buttonNode = {
cssType: 'button',
id: 'id-5',
cssClasses: new Set(['accent', 'cls-1', 'cls-2', 'cls-30']),
cssPseudoClasses: new Set<string>(),
};

const plainLabelNode = {
cssType: 'label',
cssClasses: new Set<string>(),
cssPseudoClasses: new Set<string>(),
};

// Simulates list recycling: rows cycle through a small set of distinct
// (type, classes) signatures, so candidate lookups repeat constantly.
const recycledRows = Array.from({ length: 20 }, (_, i) => ({
cssType: i % 2 === 0 ? 'label' : 'stacklayout',
cssClasses: new Set(['accent', `cls-${i % 5}`, `variant-${i % 3}`, `variant-${7 + (i % 2)}`]),
cssPseudoClasses: new Set<string>(),
}));

describe('StyleSheetSelectorScope.query', () => {
bench('query - button with id and 4 classes', () => {
scope.query(buttonNode);
});

bench('query - plain label', () => {
scope.query(plainLabelNode);
});

bench('query - 20 recycled list rows', () => {
for (let i = 0; i < recycledRows.length; i++) {
scope.query(recycledRows[i]);
}
});
});

describe('selector match', () => {
const sequenceSelector = createSelector('button.accent.cls-1');
const sequenceNode = { cssType: 'button', cssClasses: new Set(['accent', 'cls-1']) };

bench('SimpleSelectorSequence.match', () => {
sequenceSelector.match(sequenceNode);
});

const complexSelector = createSelector('stacklayout > label.title');
const parentNode = { cssType: 'stacklayout', cssClasses: new Set<string>() };
const childNode = { cssType: 'label', cssClasses: new Set(['title']), parent: parentNode };

bench('ComplexSelector.match (child combinator)', () => {
complexSelector.match(childNode);
});

const attributeSelector = new AttributeSelector('testAttr', 'equals', 'SOME-value', true);
const attributeNode = { cssType: 'button', testAttr: 'some-VALUE' };

bench('AttributeSelector.match (ignoreCase)', () => {
attributeSelector.match(attributeNode);
});
});
78 changes: 78 additions & 0 deletions packages/core/__tests__/benchmarks/observable.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { bench, describe } from 'vitest';
import { Observable, EventData } from '../../data/observable';

class TestObservable extends Observable {}

function noop(_data: EventData) {
// listener body intentionally empty
}

describe('Observable.notify', () => {
const singleListener = new TestObservable();
singleListener.on('tick', noop);

bench('notify - single listener (no thisArg)', () => {
singleListener.notify({ eventName: 'tick', object: singleListener });
});

const singleListenerThisArg = new TestObservable();
const ctx = { name: 'ctx' };
singleListenerThisArg.on('tick', noop, ctx);

bench('notify - single listener (with thisArg)', () => {
singleListenerThisArg.notify({ eventName: 'tick', object: singleListenerThisArg });
});

const multiListener = new TestObservable();
multiListener.on('tick', noop, { id: 1 });
multiListener.on('tick', noop, { id: 2 });
multiListener.on('tick', noop, { id: 3 });

bench('notify - three listeners (with thisArg)', () => {
multiListener.notify({ eventName: 'tick', object: multiListener });
});

const noListeners = new TestObservable();

bench('notify - no listeners', () => {
noListeners.notify({ eventName: 'tick', object: noListeners });
});

const propertyChangeTarget = new TestObservable();
propertyChangeTarget.on(Observable.propertyChangeEvent, noop);

bench('notifyPropertyChange', () => {
propertyChangeTarget.notifyPropertyChange('value', 1, 0);
});
});

const ctxLast = { id: 'last' };

describe('Observable listener management', () => {
const observable = new TestObservable();

bench('hasListeners - present', () => {
observable.hasListeners('registered');
});

observable.on('registered', noop);

bench('hasListeners - absent', () => {
observable.hasListeners('unregistered');
});

bench('on/off cycle', () => {
observable.on('cycle', noop);
observable.off('cycle', noop);
});

const manyListeners = new TestObservable();
for (let i = 0; i < 10; i++) {
manyListeners.on('busy', noop, { id: i });
}

bench('on/off cycle - 10 existing listeners', () => {
manyListeners.on('busy', noop, ctxLast);
manyListeners.off('busy', noop, ctxLast);
});
});
30 changes: 30 additions & 0 deletions packages/core/__tests__/benchmarks/properties.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { bench, describe } from 'vitest';
import { isCssWideKeyword, isResetValue, unsetValue } from '../../ui/core/properties/property-shared';

const objectValue = { some: 'value' };

describe('property value helpers', () => {
bench('isResetValue - number value', () => {
isResetValue(42);
});

bench('isResetValue - object value', () => {
isResetValue(objectValue);
});

bench('isResetValue - regular string value', () => {
isResetValue('center');
});

bench('isResetValue - unsetValue', () => {
isResetValue(unsetValue);
});

bench('isCssWideKeyword - number value', () => {
isCssWideKeyword(42);
});

bench('isCssWideKeyword - regular string value', () => {
isCssWideKeyword('center');
});
});
5 changes: 3 additions & 2 deletions packages/core/css/CSS3Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,8 @@ export class CSS3Parser {
text: undefined,
components: [],
};
do {
// eslint-disable-next-line no-constant-condition
while (true) {
if (this.nextInputCodePointIndex >= this.text.length) {
funcToken.text = name + '(' + this.text.substring(start);

Expand All @@ -692,6 +693,6 @@ export class CSS3Parser {
}
// TODO: Else we won't advance
}
} while (true);
}
}
}
103 changes: 103 additions & 0 deletions packages/core/data/observable/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,107 @@ describe('Observable', () => {
expect(callCount2).toBe(1);
});
});

describe('notify', () => {
it('calls listeners in registration order with the event data', () => {
const observable = new Observable();
const calls: string[] = [];
observable.on('test', (data) => calls.push('first:' + data.eventName));
observable.on('test', (data) => calls.push('second:' + data.eventName));
observable.notify({ eventName: 'test', object: observable });
expect(calls).toEqual(['first:test', 'second:test']);
});

it('binds thisArg as the callback context', () => {
const observable = new Observable();
const ctx = { name: 'ctx' };
let receivedThis: any;
let receivedData: any;
observable.on(
'test',
function (this: any, data) {
receivedThis = this;
receivedData = data;
},
ctx,
);
observable.notify({ eventName: 'test', object: observable });
expect(receivedThis).toBe(ctx);
expect(receivedData.eventName).toBe('test');
expect(receivedData.object).toBe(observable);
});

it('allows a listener to remove itself during dispatch without affecting other listeners', () => {
const observable = new Observable();
const calls: string[] = [];
const first = () => {
calls.push('first');
observable.off('test', first);
};
const second = () => calls.push('second');
observable.on('test', first);
observable.on('test', second);
observable.notify({ eventName: 'test', object: observable });
observable.notify({ eventName: 'test', object: observable });
expect(calls).toEqual(['first', 'second', 'second']);
});
});

describe('hasListeners', () => {
it('reflects listener registration and removal', () => {
const observable = new Observable();
expect(observable.hasListeners('test')).toBe(false);
const handler = () => {
// no-op
};
observable.on('test', handler);
expect(observable.hasListeners('test')).toBe(true);
observable.off('test', handler);
expect(observable.hasListeners('test')).toBe(false);
});
});

describe('static (global) event handlers', () => {
afterEach(() => {
Observable.removeEventListener('globalTest');
});

it('notifies global handlers registered on Observable for any instance', () => {
const calls: string[] = [];
Observable.addEventListener('globalTest', () => calls.push('global'));

const observable = new Observable();
observable.on('globalTest', () => calls.push('instance'));
observable.notify({ eventName: 'globalTest', object: observable });

expect(calls).toEqual(['instance', 'global']);
});

it('notifies eventNameFirst global handlers before instance listeners', () => {
const calls: string[] = [];
Observable.addEventListener('globalTestFirst', () => calls.push('globalFirst'));

const observable = new Observable();
observable.on('globalTest', () => calls.push('instance'));
observable.notify({ eventName: 'globalTest', object: observable });

Observable.removeEventListener('globalTestFirst');

expect(calls).toEqual(['globalFirst', 'instance']);
});

it('stops notifying after global handler removal', () => {
let callCount = 0;
const handler = () => callCount++;
Observable.addEventListener('globalTest', handler);

const observable = new Observable();
observable.notify({ eventName: 'globalTest', object: observable });
expect(callCount).toBe(1);

Observable.removeEventListener('globalTest', handler);
observable.notify({ eventName: 'globalTest', object: observable });
expect(callCount).toBe(1);
});
});
});
Loading
Loading