@@ -46,13 +47,13 @@ class ContextWorker extends Nullstack {
-
+
-
id)?.join(',')}>
+
id)?.join(',')}>
{worker.registration &&
diff --git a/tests/src/ContextWorker.test.js b/tests/src/ContextWorker.test.js
index 9043041b..bd21572f 100644
--- a/tests/src/ContextWorker.test.js
+++ b/tests/src/ContextWorker.test.js
@@ -15,7 +15,7 @@ describe('ContextWorker', () => {
});
test('has a cdn key', async () => {
- const element = await page.$('[data-cdn="http://127.0.0.1:6969"]');
+ const element = await page.$('[data-cdn="http://localhost:6969"]');
expect(element).toBeTruthy();
});
@@ -74,7 +74,7 @@ describe('ContextWorker', () => {
describe('ContextWorker', () => {
beforeAll(async () => {
- await page.goto('http://localhost:6969/context-worker');
+ await page.goto('http://localhost:6969/context-worker', { waitUntil: "networkidle0" });
});
test('fetching is set to true when the worker is fetching', async () => {
@@ -90,7 +90,7 @@ describe('ContextWorker', () => {
describe('ContextWorker', () => {
beforeAll(async () => {
- await page.goto('http://localhost:6969/context-worker');
+ await page.goto('http://localhost:6969/context-worker', { waitUntil: "networkidle0" });
});
test('fetching is set to the arguments of the server function when the worker is fetching', async () => {
diff --git a/tests/src/DateParser.njs b/tests/src/DateParser.njs
index fb118093..70e6c737 100644
--- a/tests/src/DateParser.njs
+++ b/tests/src/DateParser.njs
@@ -3,20 +3,20 @@ import Nullstack from 'nullstack';
class DateParser extends Nullstack {
object = null;
-
+
prepare(context) {
- const date = new Date('1992-10-16');
- context.object = {date}
- this.object = {date};
+ const date = new Date('1992-10-16');
+ context.object = { date }
+ this.object = { date };
}
-
- render({self, object}) {
+
+ render({ object }) {
return (
-
+
- {self.hydrated &&
}
- {self.hydrated &&
}
+ {this.hydrated &&
}
+ {this.hydrated &&
}
)
}
diff --git a/tests/src/DynamicHead.njs b/tests/src/DynamicHead.njs
new file mode 100644
index 00000000..9705f975
--- /dev/null
+++ b/tests/src/DynamicHead.njs
@@ -0,0 +1,73 @@
+import Nullstack from 'nullstack';
+
+class DynamicHead extends Nullstack {
+
+ count = 0
+ negativeCount = 0
+
+ renderHead() {
+ const innerComponent = `[data-inner-component] { color: blue }`
+ return (
+
+ )
+ }
+
+ render() {
+ const color = this.count % 2 === 0 ? 'red' : 'blue'
+ const redBlue = `[data-red-blue] { color: ${color}}`
+ const prerenderConditional = `[data-prerender-conditional] { color: blue }`
+ const rerenderConditional = `[data-rerender-conditional] { color: blue }`
+ const fragment = `[data-fragment] { color: blue }`
+ const conditionalHead = `[data-conditional-head] { color: blue }`
+ const dynamicLength = `[data-dynamic-length] { color: blue }`
+ return (
+
+
+
+ {this.count === 0 && }
+ {this.count === 1 && }
+ <>
+
+ >
+
+
+ {this.count === 1 &&
+
+
+
+ }
+
+ {this.count % 2 === 0 ? :
}
+
+
+ {Array(this.count + 1 - this.negativeCount).fill()}
+
+
+
+
data-red-blue
+
data-prerender-conditional
+
data-rerender-conditional
+
data-fragment
+
data-conditional-head
+
data-inner-component
+ {this.count % 2 === 0 ?
+
+
+ :
not head
+ }
+ {this.count % 2 === 0
+ ?
+
+
+ :
+
+
+
+ }
+
+ )
+ }
+
+}
+
+export default DynamicHead;
\ No newline at end of file
diff --git a/tests/src/DynamicHead.test.js b/tests/src/DynamicHead.test.js
new file mode 100644
index 00000000..cfca3008
--- /dev/null
+++ b/tests/src/DynamicHead.test.js
@@ -0,0 +1,135 @@
+beforeEach(async () => {
+ await page.goto('http://localhost:6969/dynamic-head');
+});
+
+describe('DynamicHead', () => {
+
+ test('styles can be added inline to the head tag during ssr', async () => {
+ const color = await page.evaluate('getComputedStyle(document.body.querySelector("[data-red-blue]")).color')
+ expect(color).toEqual('rgb(255, 0, 0)');
+ });
+
+ test('head styles can be updated dynamicly', async () => {
+ await page.click('[data-increment]');
+ await page.waitForSelector('[data-red-blue][data-count="1"]');
+ const color = await page.evaluate('getComputedStyle(document.body.querySelector("[data-red-blue]")).color')
+ expect(color).toEqual('rgb(0, 0, 255)');
+ });
+
+ test('styles can be added from inner components to the head tag', async () => {
+ await page.waitForSelector('[data-inner-component]');
+ const color = await page.evaluate('getComputedStyle(document.body.querySelector("[data-inner-component]")).color')
+ expect(color).toEqual('rgb(0, 0, 255)');
+ });
+
+ test('styles can be added from fragments to the head tag', async () => {
+ await page.waitForSelector('[data-fragment]');
+ const color = await page.evaluate('getComputedStyle(document.body.querySelector("[data-fragment]")).color')
+ expect(color).toEqual('rgb(0, 0, 255)');
+ });
+
+ test('styles can be conditionaly prerendered inside the head tag', async () => {
+ const color = await page.evaluate('getComputedStyle(document.body.querySelector("[data-prerender-conditional]")).color')
+ expect(color).toEqual('rgb(0, 0, 255)');
+ });
+
+ test('styles can be conditionaly rerendered inside the head tag', async () => {
+ await page.click('[data-increment]');
+ await page.waitForSelector('head [data-rerender-conditional]');
+ const color = await page.evaluate('getComputedStyle(document.body.querySelector("[data-rerender-conditional]")).color')
+ expect(color).toEqual('rgb(0, 0, 255)');
+ });
+
+ test('styles can be rendered inside a conditionaly rendered head tag', async () => {
+ await page.click('[data-increment]');
+ await page.waitForSelector('head [data-conditional-head]');
+ const color = await page.evaluate('getComputedStyle(document.body.querySelector("[data-conditional-head]")).color')
+ expect(color).toEqual('rgb(0, 0, 255)');
+ });
+
+ test('heads can be replaced by ternaries', async () => {
+ await page.click('[data-increment]');
+ await page.waitForSelector('[data-ternary-span]');
+ const element = await page.$('[data-ternary-span]');
+ expect(element).toBeTruthy();
+ });
+
+ test('heads can be inserted by ternaries', async () => {
+ await page.click('[data-increment]');
+ await page.waitForSelector('[data-ternary-span]');
+ await page.click('[data-increment]');
+ await page.waitForSelector('[data-ternary-head]');
+ const element = await page.$('[data-ternary-head]');
+ expect(element).toBeTruthy();
+ });
+
+ test('head elements can have custom ids', async () => {
+ const element = await page.$('#ternary-head');
+ expect(element).toBeTruthy();
+ });
+
+ test('the head tag accepts dynamic lists of increasing size', async () => {
+ for (let i = 1; i < 3; i++) {
+ await page.click('[data-increment]');
+ await page.waitForSelector(`[data-dynamic-length="${i}"]`);
+ }
+ const elements = await page.$$('[data-dynamic-length]');
+ expect(elements.length).toEqual(3);
+ });
+
+ test('the head tag accepts dynamic lists of decreasing size', async () => {
+ for (let i = 1; i < 3; i++) {
+ await page.click('[data-increment]');
+ await page.waitForSelector(`[data-dynamic-length="${i}"]`);
+ }
+ await page.click('[data-decrement]');
+ await page.waitForSelector('[data-negative-count]');
+ const elements = await page.$$('[data-negative-count]');
+ expect(elements.length).toEqual(2);
+ });
+
+ test('dynamic head elements can update attributes', async () => {
+ for (let i = 1; i < 3; i++) {
+ await page.click('[data-increment]');
+ await page.waitForSelector(`[data-dynamic-length="${i}"]`);
+ }
+ await page.click('[data-decrement]');
+ await page.waitForSelector('[data-negative-count]');
+ await page.click('[data-decrement]');
+ await page.waitForSelector('[data-dynamic-length]:not([data-negative-count])');
+ const element = await page.$('[data-dynamic-length]:not([data-negative-count])');
+ expect(element).toBeTruthy();
+ });
+
+ test('heads can be replaced in ternaries by heads with a highter children length', async () => {
+ await page.click('[data-increment]');
+ await page.waitForSelector('[data-b2]');
+ await page.waitForSelector('[data-b1]');
+ const a1 = await page.$('[data-a1]');
+ const b1 = await page.$('[data-b1]');
+ const b2 = await page.$('[data-b2]');
+ expect(!a1 && b1 && b2).toBeTruthy();
+ });
+
+ test('heads can be replaced in ternaries by heads with a highter children length', async () => {
+ await page.click('[data-increment]');
+ await page.waitForSelector('[data-b2]');
+ await page.waitForSelector('[data-b1]');
+ await page.click('[data-increment]');
+ await page.waitForSelector('[data-a1]');
+ const a1 = await page.$('[data-a1]');
+ const b1 = await page.$('[data-b1]');
+ const b2 = await page.$('[data-b2]');
+ expect(a1 && !b1 && !b2).toBeTruthy();
+ });
+
+ test('head children can be ternaries', async () => {
+ await page.click('[data-increment]');
+ await page.waitForSelector('meta[data-ternary-head-children]');
+ await page.click('[data-increment]');
+ await page.waitForSelector('style[data-ternary-head-children]');
+ const element = await page.$('style[data-ternary-head-children]');
+ expect(element).toBeTruthy();
+ });
+
+});
\ No newline at end of file
diff --git a/tests/src/Element.njs b/tests/src/Element.njs
index 923e9c97..e9755005 100644
--- a/tests/src/Element.njs
+++ b/tests/src/Element.njs
@@ -1,14 +1,39 @@
import Nullstack from 'nullstack';
+class ClassElement extends Nullstack {
+
+ render({ prop }) {
+ return (
+
+ )
+ }
+
+}
+
+function FunctionalElement({ prop }) {
+ return (
+
+ )
+}
+
class Element extends Nullstack {
-
+
+ renderInnerElement({ prop }) {
+ return (
+
+ )
+ }
+
render() {
return (
- <>
+ <>
b
abbr
+
+
+
>
)
}
diff --git a/tests/src/Element.test.js b/tests/src/Element.test.js
index 6947b7f0..d5606a4a 100644
--- a/tests/src/Element.test.js
+++ b/tests/src/Element.test.js
@@ -19,4 +19,20 @@ describe('FullStackLifecycle', () => {
expect(element).toBeFalsy();
});
+ test('elements can be inner components and receive props', async () => {
+ const element = await page.$('[data-inner-component]');
+ expect(element).toBeTruthy();
+ });
+
+ test('elements can be class components and receive props', async () => {
+ const element = await page.$('[data-class-component]');
+ expect(element).toBeTruthy();
+ });
+
+ test('elements can be functional components and receive props', async () => {
+ const element = await page.$('[data-functional-component]');
+ expect(element).toBeTruthy();
+ });
+
+
});
\ No newline at end of file
diff --git a/tests/src/ErrorOnChildNode.njs b/tests/src/ErrorOnChildNode.njs
index 7ff32ce5..86b9a77c 100644
--- a/tests/src/ErrorOnChildNode.njs
+++ b/tests/src/ErrorOnChildNode.njs
@@ -14,14 +14,14 @@ class ObjectId {
class ErrorOnChildNode extends Nullstack {
- testValue = 'initial Value';
+ value = 'initial Value';
records = [
{ _id: new ObjectId('a') },
]
testClick() {
- this.testValue = 'Changed Value';
+ this.value = 'Changed Value';
}
renderSerializationError() {
@@ -61,13 +61,13 @@ class ErrorOnChildNode extends Nullstack {
render({ params }) {
return (
<>
-
Table Error
+
Table Error
{params.serialization &&
}
{params.dom &&
}
-
- {this.testValue}
+
+ {this.value}
-
+
>
);
}
diff --git a/tests/src/ErrorOnChildNode.test.js b/tests/src/ErrorOnChildNode.test.js
index 9e34a577..5171e575 100644
--- a/tests/src/ErrorOnChildNode.test.js
+++ b/tests/src/ErrorOnChildNode.test.js
@@ -1,33 +1,42 @@
-describe('ErrorOnChildNode dom', () => {
-
- let error;
-
- beforeAll(async () => {
- jest.spyOn(console, 'error').mockImplementation((message) => error = message);
- await page.goto('http://localhost:6969/error-on-child-node?dom=true');
- });
+describe('ErrorOnChildNode dom ssr', () => {
test('should log that the dom is invalid', async () => {
- page.on("console", async () => {
- expect(error).toMatch('THEAD expected tag TH to be child at index 2 but instead found undefined. This error usually happens because of an invalid HTML hierarchy or changes in comparisons after serialization.');
+ const page = await browser.newPage();
+ await page.goto('http://localhost:6969/error-on-child-node?dom=true', { waitUntil: "networkidle0" });
+ page.on("console", async (message) => {
+ expect(message.text()).toMatch('THEAD expected tag TH to be child at index 2 but instead found undefined. This error usually happens because of an invalid HTML hierarchy or changes in comparisons after serialization.');
})
});
-})
-describe('ErrorOnChildNode serialization', () => {
-
- let error;
+ test('Should log that the serialization missmatches the server dom', async () => {
+ const page = await browser.newPage();
+ await page.goto('http://localhost:6969/error-on-child-node?serialization=true', { waitUntil: "networkidle0" });
+ page.on("console", async (message) => {
+ expect(message.text()).toMatch('DIV expected tag DIV to be child at index 0 but instead found undefined. This error usually happens because of an invalid HTML hierarchy or changes in comparisons after serialization.');
+ })
+ });
- beforeAll(async () => {
- jest.spyOn(console, 'error').mockImplementation((message) => error = message);
- await page.goto('http://localhost:6969/error-on-child-node?serialization=true');
+ test('hydration errors related to dom should not happen in spa mode', async () => {
+ const page = await browser.newPage();
+ await page.goto('http://localhost:6969/', { waitUntil: "networkidle0" });
+ await page.click('[href="/error-on-child-node?dom=true"]');
+ await page.waitForSelector('[data-dom-error]');
+ await page.click('[data-dom-error]');
+ await page.waitForSelector('[data-value="Changed Value"]');
+ const element = await page.$('[data-value="Changed Value"]');
+ expect(element).toBeTruthy();
});
- test('Should log that the serialization missmatches the server dom', async () => {
- page.on("console", async () => {
- expect(error).toMatch('DIV expected tag DIV to be child at index 0 but instead found undefined. This error usually happens because of an invalid HTML hierarchy or changes in comparisons after serialization.');
- })
+ test('hydration errors related to dom should not happen in spa mode', async () => {
+ const page = await browser.newPage();
+ await page.goto('http://localhost:6969/', { waitUntil: "networkidle0" });
+ await page.click('[href="/error-on-child-node?serialization=true"]');
+ await page.waitForSelector('[data-dom-error]');
+ await page.click('[data-dom-error]');
+ await page.waitForSelector('[data-value="Changed Value"]');
+ const element = await page.$('[data-value="Changed Value"]');
+ expect(element).toBeTruthy();
});
})
\ No newline at end of file
diff --git a/tests/src/ErrorPage.njs b/tests/src/ErrorPage.njs
index bc05bbc7..ae4cf6ec 100644
--- a/tests/src/ErrorPage.njs
+++ b/tests/src/ErrorPage.njs
@@ -2,8 +2,8 @@ import Nullstack from 'nullstack';
class ErrorPage extends Nullstack {
- async initiate({page, params}) {
- if(params.status == 500) {
+ async initiate({ page, params }) {
+ if (params.status === '500') {
this.error.simulate;
} else {
page.status = 404;
diff --git a/tests/src/ErrorPage.test.js b/tests/src/ErrorPage.test.js
index 81063b74..5c0d55fb 100644
--- a/tests/src/ErrorPage.test.js
+++ b/tests/src/ErrorPage.test.js
@@ -3,8 +3,8 @@ describe('ErrorPage 500', () => {
let response;
beforeAll(async () => {
- response = await page.goto('http://localhost:6969/error-page?status=500');
- });
+ response = await page.goto('http://localhost:6969/error-page?status=500', { waitUntil: "networkidle0" });
+ });
test('pages with error have a 500 status', async () => {
const status = response.status();
@@ -22,7 +22,7 @@ describe('ErrorPage 500', () => {
describe('ErrorPage 404', () => {
test('pages with status 404 have a 404 status', async () => {
- const response = await page.goto('http://localhost:6969/error-page');
+ const response = await page.goto('http://localhost:6969/error-page', { waitUntil: "networkidle0" });
const status = response.status();
expect(status).toBe(404);
});
@@ -38,9 +38,9 @@ describe('ErrorPage 404', () => {
describe('ErrorPage offline', () => {
test('the offline template should always have a 200 status', async () => {
- await page.goto('http://localhost:6969');
+ await page.goto('http://localhost:6969', { waitUntil: "networkidle0" });
const link = await page.$eval('a', (element) => element.href);
- const response = await page.goto(link);
+ const response = await page.goto(link, { waitUntil: "networkidle0" });
const status = response.status();
expect([200, 304]).toContain(status);
});
diff --git a/tests/src/FalsyNodes.njs b/tests/src/FalsyNodes.njs
index 1732bca5..38a44212 100644
--- a/tests/src/FalsyNodes.njs
+++ b/tests/src/FalsyNodes.njs
@@ -6,11 +6,21 @@ class FalsyNodes extends Nullstack {
zeroNode = 0;
falseNode = false;
+ renderNullComponent() {
+ return null
+ }
+
+ renderFalseComponent() {
+ return false
+ }
+
render() {
return (
<>
{this.nullNode}
{this.falseNode}
+
+
{this.zeroNode}
>
)
diff --git a/tests/src/FalsyNodes.test.js b/tests/src/FalsyNodes.test.js
index 6a337339..56ee7b4f 100644
--- a/tests/src/FalsyNodes.test.js
+++ b/tests/src/FalsyNodes.test.js
@@ -14,8 +14,18 @@ describe('Falsy Nodes', () => {
expect(truth).toBeTruthy();
});
+ test('Components returning null should render a comment', async () => {
+ const truth = await page.$eval('[data-null-component]', (e) => e.childNodes[0] instanceof Comment);
+ expect(truth).toBeTruthy();
+ });
+
+ test('Components returning false should render a comment', async () => {
+ const truth = await page.$eval('[data-false-component]', (e) => e.childNodes[0] instanceof Comment);
+ expect(truth).toBeTruthy();
+ });
+
test('Zero nodes should render a text node', async () => {
- const truth = await page.$eval('[data-zero]', (e) => e.innerText === '0');
+ const truth = await page.$eval('[data-zero]', (e) => e.textContent === '0');
expect(truth).toBeTruthy();
});
diff --git a/tests/src/FullStackLifecycle.test.js b/tests/src/FullStackLifecycle.test.js
index f17af82b..a9c436de 100644
--- a/tests/src/FullStackLifecycle.test.js
+++ b/tests/src/FullStackLifecycle.test.js
@@ -54,7 +54,7 @@ describe('FullStackLifecycle ssr', () => {
test('terminate should run', async () => {
await page.click('a[href="/"]');
- await page.waitForFunction(() => location.search == '?terminated=true');
+ await page.waitForFunction(() => location.search === '?terminated=true');
const element = await page.$('.FullStackLifecycle');
expect(element).toBeFalsy();
});
@@ -118,7 +118,7 @@ describe('FullStackLifecycle spa', () => {
test('terminate should run', async () => {
await page.click('a[href="/"]');
- await page.waitForFunction(() => location.search == '?terminated=true');
+ await page.waitForFunction(() => location.search === '?terminated=true');
const element = await page.$('.FullStackLifecycle');
expect(element).toBeFalsy();
});
diff --git a/tests/src/HydrateElement.njs b/tests/src/HydrateElement.njs
deleted file mode 100644
index 74ca661f..00000000
--- a/tests/src/HydrateElement.njs
+++ /dev/null
@@ -1,17 +0,0 @@
-import Nullstack from 'nullstack';
-
-class HydrateElement extends Nullstack {
-
- hydrate({ self }) {
- this.id = self.element.id
- }
-
- render() {
- return (
-
HydrateElement
- )
- }
-
-}
-
-export default HydrateElement;
\ No newline at end of file
diff --git a/tests/src/HydrateElement.test.js b/tests/src/HydrateElement.test.js
deleted file mode 100644
index cd930528..00000000
--- a/tests/src/HydrateElement.test.js
+++ /dev/null
@@ -1,15 +0,0 @@
-describe('FullStackLifecycle ssr', () => {
-
- beforeAll(async () => {
- await page.goto('http://localhost:6969/');
- await page.click('[href="/hydrate-element"]')
- });
-
- test('prepare should run', async () => {
- await page.waitForSelector('[data-id="hydrate-element"]');
- const element = await page.$('[data-id="hydrate-element"]');
- expect(element).toBeTruthy();
- });
-
-
-});
\ No newline at end of file
diff --git a/tests/src/InstanceSelf.njs b/tests/src/InstanceSelf.njs
index 26e129d1..180e9ef4 100644
--- a/tests/src/InstanceSelf.njs
+++ b/tests/src/InstanceSelf.njs
@@ -3,26 +3,14 @@ import StatefulComponent from './StatefulComponent';
class InstanceSelf extends Nullstack {
- optimize = false;
-
- hydrate({self}) {
- this.className = self.element.className;
- }
-
- render({self}) {
+ render() {
return (
-
-
-
-
- {self.hydrated &&
- <>
-
-
-
- >
- }
+
+
+
+
+ {this.hydrated &&
}
)
}
diff --git a/tests/src/InstanceSelf.test.js b/tests/src/InstanceSelf.test.js
index ef3ce280..067e64b5 100644
--- a/tests/src/InstanceSelf.test.js
+++ b/tests/src/InstanceSelf.test.js
@@ -4,37 +4,31 @@ beforeAll(async () => {
describe('InstanceSelf', () => {
- test('self is aware that the component initiated', async () => {
+ test('instance is aware that the component initiated', async () => {
await page.waitForSelector('[data-initiated]');
const element = await page.$('[data-initiated]');
expect(element).toBeTruthy();
});
- test('self is aware that the component is hydrated', async () => {
+ test('instance is aware that the component is hydrated', async () => {
await page.waitForSelector('[data-hydrated]');
const element = await page.$('[data-hydrated]');
expect(element).toBeTruthy();
});
- test('self is aware that the component was prerendered', async () => {
+ test('instance is aware that the component was prerendered', async () => {
await page.waitForSelector('[data-prerendered]');
const element = await page.$('[data-prerendered]');
expect(element).toBeTruthy();
});
- test('self is aware of the component element', async () => {
- await page.waitForSelector('[data-class-name="InstanceSelf"]');
- const element = await page.$('[data-class-name="InstanceSelf"]');
- expect(element).toBeTruthy();
- });
-
- test('self key has route appended to depth if no key is declared', async () => {
+ test('instance key has route appended to depth if no key is declared', async () => {
await page.waitForSelector('[data-key="InstanceSelf/0-0-6/instance-self"]');
const element = await page.$('[data-key="InstanceSelf/0-0-6/instance-self"]');
expect(element).toBeTruthy();
});
- test('self is aware of the component element after render', async () => {
+ test('instance is aware of the component element after render', async () => {
await page.waitForSelector('[data-tag="form"]');
const element = await page.$('[data-tag="form"]');
expect(element).toBeTruthy();
diff --git a/tests/src/Instanceable.njs b/tests/src/Instanceable.njs
index 93cbc5e6..7b882d3c 100644
--- a/tests/src/Instanceable.njs
+++ b/tests/src/Instanceable.njs
@@ -39,22 +39,21 @@ class Instanceable extends Nullstack {
instanceable.serverLoaded = true;
}
- renderTitle({ title, self }) {
+ renderTitle({ title }) {
return (
title[0] !== '_isProxy' &&
-
+
{this.title[title[0]]}
)
}
- render({ instances, self }) {
+ render({ instances }) {
const { application } = instances;
const mainHasKey = (
application &&
typeof application.changeInstanceable === 'function'
);
-
return (
{application &&
@@ -71,7 +70,7 @@ class Instanceable extends Nullstack {
}
-
+
)
}
diff --git a/tests/src/Instanceable.test.js b/tests/src/Instanceable.test.js
index 25bd5fc0..3270d0ff 100644
--- a/tests/src/Instanceable.test.js
+++ b/tests/src/Instanceable.test.js
@@ -8,7 +8,7 @@ describe('Instanceable', () => {
const selector = `[data-title="${title}"][data-hydrated]`;
await page.waitForSelector(selector);
const p = await page.$(selector);
- const text = await page.evaluate(element => element.innerText, p);
+ const text = await page.evaluate(element => element.textContent, p);
const expectedValue = new Array(2).fill(value).join(' ');
expect(text).toBe(expectedValue);
@@ -33,7 +33,7 @@ describe('Instanceable', () => {
expect(p).toBeTruthy();
});
- test('self key ignores the route suffix if a key is declared', async () => {
+ test('instance key ignores the route suffix if a key is declared', async () => {
await page.waitForSelector('[data-key="instanceable"]');
const element = await page.$('[data-key="instanceable"]');
expect(element).toBeTruthy();
diff --git a/tests/src/LazyComponentLoader.njs b/tests/src/LazyComponentLoader.njs
index a1664ba6..f60ed269 100644
--- a/tests/src/LazyComponentLoader.njs
+++ b/tests/src/LazyComponentLoader.njs
@@ -13,8 +13,8 @@ class LazyComponentLoader extends Nullstack {
LazyComponent = (await import('./LazyComponent')).default
}
- render({ self }) {
- if (!self.hydrated) return false
+ render() {
+ if (!this.hydrated) return false
return
}
diff --git a/tests/src/NestedProxy.njs b/tests/src/NestedProxy.njs
index d99859b2..7af544e3 100644
--- a/tests/src/NestedProxy.njs
+++ b/tests/src/NestedProxy.njs
@@ -1,9 +1,19 @@
import Nullstack from 'nullstack';
+class ShouldNotProxy {
+
+ something = false
+
+ setSomething(value) {
+ this.something = value
+ }
+
+}
+
class NestedProxy extends Nullstack {
array = [
- {object: {object: true}}
+ { object: { object: true } }
]
object = {
@@ -12,28 +22,37 @@ class NestedProxy extends Nullstack {
prepare(context) {
context.array = [
- {object: {object: true}}
+ { object: { object: true } }
]
context.object = {
array: [true]
}
}
-
- render(context) {
- if(!context.self.hydrated) return false
+
+ hydrate(context) {
+ this.shouldNotProxy = new ShouldNotProxy()
+ this.shouldNotProxy.setSomething(true)
+ context.shouldNotProxy = new ShouldNotProxy()
+ context.shouldNotProxy.setSomething(true)
+ }
+
+ render({ array, object, shouldNotProxy }) {
+ if (!this.hydrated) return false
return (
-
)
}
diff --git a/tests/src/NestedProxy.test.js b/tests/src/NestedProxy.test.js
index c47ddaaa..1f7fc69f 100644
--- a/tests/src/NestedProxy.test.js
+++ b/tests/src/NestedProxy.test.js
@@ -34,34 +34,46 @@ describe('ParentComponent', () => {
expect(element).toBeTruthy();
});
+ test('non direct object instances nested to instance do not become proxies', async () => {
+ await page.waitForSelector('[data-should-not-proxy]');
+ const element = await page.$('[data-should-not-proxy]');
+ expect(element).toBeTruthy();
+ });
+
test('context arrays become proxies', async () => {
await page.waitForSelector('[data-context-array]');
const element = await page.$('[data-context-array]');
expect(element).toBeTruthy();
});
-
+
test('objects nested to context arrays become proxies', async () => {
await page.waitForSelector('[data-context-array-zero]');
const element = await page.$('[data-context-array-zero]');
expect(element).toBeTruthy();
});
-
+
test('objects nested to context arrays become proxies', async () => {
await page.waitForSelector('[data-context-array-zero-object]');
const element = await page.$('[data-context-array-zero-object]');
expect(element).toBeTruthy();
});
-
+
test('context objects become proxies', async () => {
await page.waitForSelector('[data-context-object]');
const element = await page.$('[data-context-object]');
expect(element).toBeTruthy();
});
-
+
test('arrays nested to context objects become proxies', async () => {
await page.waitForSelector('[data-context-object-array]');
const element = await page.$('[data-context-object-array]');
expect(element).toBeTruthy();
});
+ test('non direct object instances nested to context do not become proxies', async () => {
+ await page.waitForSelector('[data-context-should-not-proxy]');
+ const element = await page.$('[data-context-should-not-proxy]');
+ expect(element).toBeTruthy();
+ });
+
});
\ No newline at end of file
diff --git a/tests/src/OptimizedEvents.njs b/tests/src/OptimizedEvents.njs
new file mode 100644
index 00000000..69578727
--- /dev/null
+++ b/tests/src/OptimizedEvents.njs
@@ -0,0 +1,47 @@
+import Nullstack from 'nullstack';
+
+class OptimizedEvents extends Nullstack {
+
+ count = 0
+
+ clickWhenEven() {
+ this.lastClick = 'even'
+ }
+
+ clickWhenOdd() {
+ this.lastClick = 'odd'
+ }
+
+ incrementCount() {
+ this.count++
+ }
+
+ doubleCount({ data }) {
+ this.count += data.count
+ }
+
+ eventAfterRendered() {
+ this.worksAfterRender = true
+ }
+
+ render() {
+ return (
+
+ {this.count === 1 &&
+
+
+
+ }
+
+
+
+ {this.count === 0 ? : }
+
+
+
+ )
+ }
+
+}
+
+export default OptimizedEvents;
\ No newline at end of file
diff --git a/tests/src/OptimizedEvents.test.js b/tests/src/OptimizedEvents.test.js
new file mode 100644
index 00000000..8567b939
--- /dev/null
+++ b/tests/src/OptimizedEvents.test.js
@@ -0,0 +1,77 @@
+describe('OptimizedEvents', () => {
+
+ beforeEach(async () => {
+ await page.goto('http://localhost:6969/optimized-events');
+ });
+
+ test('events are triggered', async () => {
+ await page.click('[data-increment-count]')
+ await page.waitForSelector('[data-count="1"]');
+ const element = await page.$('[data-count="1"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('events take the current attributes', async () => {
+ await page.click('[data-increment-count]')
+ await page.waitForSelector('[data-count="1"]');
+ await page.click('[data-double-count]')
+ await page.waitForSelector('[data-count="2"]');
+ const element = await page.$('[data-count="2"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('events are triggered in elements that were added late to the dom', async () => {
+ await page.click('[data-increment-count]')
+ await page.waitForSelector('[data-after-render]');
+ await page.click('[data-after-render]')
+ await page.waitForSelector('[data-works-after-rendered]');
+ const element = await page.$('[data-works-after-rendered]');
+ expect(element).toBeTruthy();
+ });
+
+ test('inline events are triggered with current values', async () => {
+ await page.click('[data-increment-count]')
+ await page.waitForSelector('[data-count="1"]');
+ await page.click('[data-set-count]')
+ await page.waitForSelector('[data-count="11"]');
+ const element = await page.$('[data-count="11"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('events can change references', async () => {
+ await page.click('[data-even-odd]')
+ await page.waitForSelector('[data-last-click="even"]');
+ const element = await page.$('[data-last-click="even"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('events can change references', async () => {
+ await page.click('[data-increment-count]')
+ await page.waitForSelector('[data-count="1"]');
+ await page.click('[data-even-odd]')
+ await page.waitForSelector('[data-last-click="odd"]');
+ const element = await page.$('[data-last-click="odd"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('events are removed if attribute becomes undefined', async () => {
+ await page.click('[data-zero-only-increment]')
+ await page.waitForSelector('[data-count="1"]');
+ await page.click('[data-zero-only-increment]')
+ await page.waitForTimeout(1000)
+ await page.waitForSelector('[data-count="1"]');
+ const element = await page.$('[data-count="1"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('events are removed in ternaries', async () => {
+ await page.click('[data-zero-nothing-increment]')
+ await page.waitForSelector('[data-count="1"]');
+ await page.click('[data-zero-nothing-increment]')
+ await page.waitForTimeout(1000)
+ await page.waitForSelector('[data-count="1"]');
+ const element = await page.$('[data-count="1"]');
+ expect(element).toBeTruthy();
+ });
+
+});
\ No newline at end of file
diff --git a/tests/src/ParentComponent.njs b/tests/src/ParentComponent.njs
index 94b6b718..c83a1818 100644
--- a/tests/src/ParentComponent.njs
+++ b/tests/src/ParentComponent.njs
@@ -1,8 +1,8 @@
import Nullstack from 'nullstack';
class ParentComponent extends Nullstack {
-
- static async getParentThis() {
+
+ static async getParentThis(context) {
return this.name;
}
diff --git a/tests/src/PersistentComponent.njs b/tests/src/PersistentComponent.njs
index 5aafb619..ad491bb0 100644
--- a/tests/src/PersistentComponent.njs
+++ b/tests/src/PersistentComponent.njs
@@ -13,8 +13,8 @@ class PersistentComponent extends Nullstack {
this.count = -1;
}
- launch({ self }) {
- if (self.initiated) {
+ launch() {
+ if (this.initiated) {
this.launchCount++
}
}
@@ -27,21 +27,17 @@ class PersistentComponent extends Nullstack {
this.count++
}
- self({ self }) {
- return self
- }
-
- render({ self, instances }) {
+ render({ instances }) {
const aCount = instances['PersistentComponent/0-0-33/persistent-component/a']?.count
- const aTerminated = instances['PersistentComponent/0-0-33/persistent-component/a']?.self?.()?.terminated
+ const aTerminated = instances['PersistentComponent/0-0-33/persistent-component/a']?.terminated
return (
a
diff --git a/tests/src/PersistentComponent.test.js b/tests/src/PersistentComponent.test.js
index 6a95a3d9..3d217897 100644
--- a/tests/src/PersistentComponent.test.js
+++ b/tests/src/PersistentComponent.test.js
@@ -10,7 +10,7 @@ describe('PersistentComponent instantiated', () => {
expect(element).toBeTruthy();
});
- test('persistent components self should have a key persistent', async () => {
+ test('persistent components instance should have a property called persistent', async () => {
await page.waitForSelector('[data-persistent]');
const element = await page.$('[data-persistent]');
expect(element).toBeTruthy();
@@ -45,7 +45,7 @@ describe('PersistentComponent terminated', () => {
expect(element).toBeTruthy();
});
- test('persistent instanes self should have a terminated key when off dom', async () => {
+ test('persistent instanes instance terminated should be true when off dom', async () => {
await page.waitForSelector('[data-a-terminated]');
const element = await page.$('[data-a-terminated]');
expect(element).toBeTruthy();
@@ -94,13 +94,13 @@ describe('PersistentComponent reinstantiated', () => {
expect(element).toBeTruthy();
});
- test('persistent components self should have a key persistent when reinstantiated', async () => {
+ test('persistent components instance should have a property called persistent when reinstantiated', async () => {
await page.waitForSelector('[data-persistent]');
const element = await page.$('[data-persistent]');
expect(element).toBeTruthy();
});
- test('persistent components self should have a key prerendered when prerendered then reinstantiated', async () => {
+ test('persistent components instance should have a property called prerendered when prerendered then reinstantiated', async () => {
await page.waitForSelector('[data-prerendered]');
const element = await page.$('[data-prerendered]');
expect(element).toBeTruthy();
diff --git a/tests/src/Refs.njs b/tests/src/Refs.njs
new file mode 100644
index 00000000..35a59b48
--- /dev/null
+++ b/tests/src/Refs.njs
@@ -0,0 +1,42 @@
+import Nullstack from 'nullstack';
+
+class Refs extends Nullstack {
+
+ composedComputed = '_composedComputed'
+
+ hydrate() {
+ this.id = this._element.id
+ }
+
+ setRef({ element, refInstanceCount, id }) {
+ this.refReceivedProps = element.id === id
+ this._function = element
+ this.isOnDOM = element.offsetHeight > 0 && refInstanceCount
+ }
+
+ renderBubble({ ref }) {
+ return (
+
+ )
+ }
+
+ changeInstance(context) {
+ context.refInstanceCount++
+ }
+
+ render({ refInstanceCount }) {
+ return (
+
+
+
+
+ span
+
+
+
+ )
+ }
+
+}
+
+export default Refs;
\ No newline at end of file
diff --git a/tests/src/Refs.test.js b/tests/src/Refs.test.js
new file mode 100644
index 00000000..b0069952
--- /dev/null
+++ b/tests/src/Refs.test.js
@@ -0,0 +1,73 @@
+describe('Refs', () => {
+
+ beforeEach(async () => {
+ await page.goto('http://localhost:6969/');
+ await page.click('[href="/refs"]')
+ });
+
+ test('refs should be loaded during hydrate', async () => {
+ await page.waitForSelector('[data-id="hydrate-element"]');
+ const element = await page.$('[data-id="hydrate-element"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('refs accept composed computed references', async () => {
+ await page.waitForSelector('[data-id="composed-computed"]');
+ const element = await page.$('[data-id="composed-computed"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('refs accept logical computed references', async () => {
+ await page.waitForSelector('[data-id="logical-computed"]');
+ const element = await page.$('[data-id="logical-computed"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('refs accept literal computed references', async () => {
+ await page.waitForSelector('[data-id="literal-computed"]');
+ const element = await page.$('[data-id="literal-computed"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('refs functions receive an element as argument', async () => {
+ await page.waitForSelector('[data-id="function"]');
+ const element = await page.$('[data-id="function"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('refs functions receive attributes as argument', async () => {
+ await page.waitForSelector('[data-ref-received-props]');
+ const element = await page.$('[data-ref-received-props]');
+ expect(element).toBeTruthy();
+ });
+
+
+ test('refs functions only run after the element is appended do DOM', async () => {
+ await page.waitForSelector('[data-dom="0"]');
+ const element = await page.$('[data-dom="0"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('refs can be bubbled down', async () => {
+ await page.waitForSelector('[data-id="bubble"]');
+ const element = await page.$('[data-id="bubble"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('refs functions run when the ref object changes', async () => {
+ await page.waitForSelector('[data-dom="0"]');
+ await page.click("button")
+ await page.waitForSelector('[data-dom="1"]');
+ const element = await page.$('[data-dom="1"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('refs are reassigned when the ref object changes ', async () => {
+ await page.waitForSelector('[data-dom="0"]');
+ await page.click("button")
+ await page.waitForSelector('[data-id="hydrate-element"][data-instance="1"]');
+ const element = await page.$('[data-id="hydrate-element"][data-instance="1"]');
+ expect(element).toBeTruthy();
+ });
+
+});
\ No newline at end of file
diff --git a/tests/src/RenderableComponent.njs b/tests/src/RenderableComponent.njs
index b3fbf2c4..ffdbf145 100644
--- a/tests/src/RenderableComponent.njs
+++ b/tests/src/RenderableComponent.njs
@@ -6,16 +6,23 @@ class RenderableComponent extends Nullstack {
return
}
- renderInnerComponent({ children }) {
+ renderInnerComponent({ children, reference: Reference }) {
return (
Inner Component
+
{children}
)
}
+ renderInnerReference({ prop }) {
+ return (
+
+ )
+ }
+
renderFalsy() {
return false;
}
@@ -37,7 +44,7 @@ class RenderableComponent extends Nullstack {
element tag
-
+
children
diff --git a/tests/src/RenderableComponent.test.js b/tests/src/RenderableComponent.test.js
index f05f71ad..307ffaf4 100644
--- a/tests/src/RenderableComponent.test.js
+++ b/tests/src/RenderableComponent.test.js
@@ -84,6 +84,11 @@ describe('RenderableComponent', () => {
expect(element).toBeFalsy();
});
+ test('inner components can be referenced and receive props', async () => {
+ const element = await page.$('[data-reference]');
+ expect(element).toBeTruthy();
+ });
+
});
describe('RenderableComponent ?condition=true', () => {
diff --git a/tests/src/RouteScroll.njs b/tests/src/RouteScroll.njs
new file mode 100644
index 00000000..f40caab5
--- /dev/null
+++ b/tests/src/RouteScroll.njs
@@ -0,0 +1,37 @@
+import Nullstack from 'nullstack';
+
+class ClassRoute extends Nullstack {
+ render() {
+ return (
+
+
#bottom
+
+ big ass div in a class
+
+
bottom div
+
+ )
+ }
+}
+
+function FunctionalRoute() {
+ return big ass div in a function
+}
+
+class RouteScroll extends Nullstack {
+
+ render({ params }) {
+ return (
+
+ )
+ }
+
+}
+
+export default RouteScroll;
\ No newline at end of file
diff --git a/tests/src/RoutesAndParams.njs b/tests/src/RoutesAndParams.njs
index a694ac96..16a073e3 100644
--- a/tests/src/RoutesAndParams.njs
+++ b/tests/src/RoutesAndParams.njs
@@ -3,6 +3,32 @@ class RoutesAndParams extends Nullstack {
paramHydrated = false;
+ initiate({ router, params }) {
+ if (params.parent) {
+ router.url = '../parent'
+ } else if (params.current) {
+ router.url = './current'
+ } else if (params.https) {
+ router.url = 'https://nullstack.app/'
+ } else if (params.http) {
+ router.url = 'http://localhost:6969/http'
+ } else if (params.same) {
+ router.url = '//localhost:6969/routes-and-params/href-same-protocol'
+ } else if (params.ftp) {
+ router.url = 'ftp://nullstack.app/'
+ } else if (params.tel) {
+ router.url = 'tel:1555696969'
+ } else if (params.mailto) {
+ router.url = 'mailto:contact@nullstack.app'
+ } else if (params.relative) {
+ router.url = 'relative'
+ } else if (params.relativeComposed) {
+ router.url = 'relative/6969'
+ } else if (params.root) {
+ router.url = '/root/6969'
+ }
+ }
+
hydrate(context) {
const { router, params } = context;
this.paramHydrated = params.id === 'a';
@@ -48,13 +74,31 @@ class RoutesAndParams extends Nullstack {
router.url = 'https://nullstack.app';
}
- render({ router, params, eventTriggered, self }) {
+ renderHrefs() {
+ return (
+ <>
+ Nullstack HTTPS
+ Nullstack HTTP
+ Nullstack SAME
+ Nullstack FTP
+ Nullstack TEL
+ Nullstack MAILTO
+ Nullstack RELATIVE
+ Nullstack RELATIVE TWO
+ Nullstack ROOT
+ Nullstack CURRENT DIR
+ Nullstack PARENT DIR
+ >
+ )
+ }
+
+ render({ router, params, eventTriggered }) {
return (
-
Nullstack
+
hash
-
+
diff --git a/tests/src/RoutesAndParams.test.js b/tests/src/RoutesAndParams.test.js
index 847fbdd0..d3162d25 100644
--- a/tests/src/RoutesAndParams.test.js
+++ b/tests/src/RoutesAndParams.test.js
@@ -1,3 +1,9 @@
+beforeAll(async () => {
+ await page.on('dialog', async dialog => {
+ await dialog.dismiss();
+ })
+});
+
describe('RoutesAndParams /routes-and-params', () => {
beforeAll(async () => {
@@ -5,7 +11,7 @@ describe('RoutesAndParams /routes-and-params', () => {
});
test('router has a base key', async () => {
- const element = await page.$('[data-base="http://localhost:6969"]');
+ const element = await page.$('[data-base="https://localhost:6969"]');
expect(element).toBeTruthy();
});
@@ -193,21 +199,6 @@ describe('RoutesAndParams /routes-and-params/d?boolean=true#hash spa', () => {
});
-describe('RoutesAndParams /routes-and-params', () => {
-
- beforeAll(async () => {
- await page.goto('http://localhost:6969/routes-and-params');
- });
-
- test('a with absolute hrefs cause a hard redirect', async () => {
- await page.click('[href="https://nullstack.app"]');
- await page.waitForSelector('[href="/contributors"]');
- const url = await page.url();
- expect(url).toMatch('https://nullstack.app');
- });
-
-});
-
describe('RoutesAndParams /routes-and-params', () => {
beforeAll(async () => {
@@ -275,4 +266,209 @@ describe('RoutesAndParams /routes-and-params/inner-html', () => {
expect(element).toBeTruthy();
});
-});
\ No newline at end of file
+});
+
+describe('RoutesAndParams /routes-and-params/hrefs spa', () => {
+
+ beforeEach(async () => {
+ await page.goto('http://localhost:6969/routes-and-params/hrefs');
+ });
+
+ test('https urls do a full redirect', async () => {
+ await Promise.all([
+ page.click('[href="https://nullstack.app/"]'),
+ page.waitForNavigation(),
+ ]);
+ const url = await page.url();
+ expect(url).toMatch('://nullstack.app');
+ });
+
+ test('http urls do a full redirect', async () => {
+ await Promise.all([
+ page.click('[href="http://nullstack.app/"]'),
+ page.waitForNavigation(),
+ ]);
+ const url = await page.url();
+ expect(url).toMatch('://nullstack.app');
+ });
+
+ test('// urls do a full redirect', async () => {
+ await Promise.all([
+ page.click('[href="//nullstack.app/"]'),
+ page.waitForNavigation(),
+ ]);
+ const url = await page.url();
+ expect(url).toMatch('://nullstack.app');
+ });
+
+ // test('ftp urls do not redirect', async () => {
+ // await page.click('[href="ftp://nullstack.app/"]'),
+ // await page.waitForSelector('[data-ftp-clicked]');
+ // const element = await page.$('[data-ftp-clicked]');
+ // expect(element).toBeTruthy();
+ // });
+
+ // test('tel urls do not redirect', async () => {
+ // await page.click('[href="tel:1555696969"]'),
+ // await page.waitForSelector('[data-tel-clicked]');
+ // const element = await page.$('[data-tel-clicked]');
+ // expect(element).toBeTruthy();
+ // });
+
+ // test('mailto urls do not redirect', async () => {
+ // await page.click('[href="mailto:contact@nullstack.app"]'),
+ // await page.waitForSelector('[data-mailto-clicked]');
+ // const element = await page.$('[data-mailto-clicked]');
+ // expect(element).toBeTruthy();
+ // });
+
+ test('relative urls redirect', async () => {
+ await Promise.all([
+ page.click('[href="relative"]'),
+ page.waitForNavigation(),
+ ])
+ const url = await page.url();
+ expect(url).toEqual('http://localhost:6969/routes-and-params/relative');
+ });
+
+ test('relative composed urls redirect', async () => {
+ await Promise.all([
+ page.click('[href="relative/6969"]'),
+ page.waitForNavigation(),
+ ])
+ const url = await page.url();
+ expect(url).toEqual('http://localhost:6969/routes-and-params/relative/6969');
+ });
+
+ test('root urls redirect', async () => {
+ await Promise.all([
+ page.click('[href="/root/6969"]'),
+ page.waitForNavigation(),
+ ])
+ const url = await page.url();
+ expect(url).toEqual('http://localhost:6969/root/6969');
+ });
+
+ test('current urls redirect', async () => {
+ await Promise.all([
+ page.click('[href="./current"]'),
+ page.waitForNavigation(),
+ ])
+ const url = await page.url();
+ expect(url).toEqual('http://localhost:6969/routes-and-params/current');
+ });
+
+ test('parent urls redirect', async () => {
+ await Promise.all([
+ page.click('[href="../parent"]'),
+ page.waitForNavigation(),
+ ])
+ const url = await page.url();
+ expect(url).toEqual('http://localhost:6969/parent');
+ });
+
+});
+
+
+// describe('RoutesAndParams /routes-and-params/hrefs ssr', () => {
+
+// test('ssr parent', async () => {
+// await Promise.all([
+// page.goto('http://localhost:6969/routes-and-params/hrefs?parent=1'),
+// page.waitForNavigation(),
+// ]);
+// const url = await page.url();
+// expect(url).toEqual('http://localhost:6969/parent');
+// });
+
+// test('ssr current', async () => {
+// await Promise.all([
+// page.goto('http://localhost:6969/routes-and-params/hrefs?current=1'),
+// page.waitForNavigation(),
+// ]);
+// const url = await page.url();
+// expect(url).toEqual('http://localhost:6969/routes-and-params/current');
+// });
+
+// test('ssr https', async () => {
+// await Promise.all([
+// page.goto('http://localhost:6969/routes-and-params/hrefs?https=1'),
+// page.waitForNavigation(),
+// ]);
+// const url = await page.url();
+// expect(url).toEqual('https://nullstack.app/');
+// });
+
+// test('ssr http', async () => {
+// await Promise.all([
+// page.goto('http://localhost:6969/routes-and-params/hrefs?http=1'),
+// page.waitForNavigation(),
+// ]);
+// const url = await page.url();
+// expect(url).toEqual('http://localhost:6969/http');
+// });
+
+// test('ssr same', async () => {
+// await Promise.all([
+// page.goto('http://localhost:6969/routes-and-params/hrefs?same=1'),
+// page.waitForNavigation(),
+// ]);
+// const url = await page.url();
+// expect(url).toEqual('http://localhost:6969/routes-and-params/href-same-protocol');
+// });
+
+// // test.skip('ssr ftp', async () => {
+// // await Promise.all([
+// // page.goto('http://localhost:6969/routes-and-params/hrefs?ftp=1'),
+// // page.waitForNavigation(),
+// // ]);
+// // const url = await page.url();
+// // expect(url).toEqual('https://nullstack.app/');
+// // });
+
+// // test.skip('ssr tel', async () => {
+// // await Promise.all([
+// // page.goto('http://localhost:6969/routes-and-params/hrefs?tel=1'),
+// // page.waitForNavigation(),
+// // ]);
+// // const url = await page.url();
+// // expect(url).toEqual('https://nullstack.app/');
+// // });
+
+// // test.skip('ssr mailto', async () => {
+// // await Promise.all([
+// // page.goto('http://localhost:6969/routes-and-params/hrefs?mailto=1'),
+// // page.waitForNavigation(),
+// // ]);
+// // const url = await page.url();
+// // expect(url).toEqual('https://nullstack.app/');
+// // });
+
+// test('ssr relative', async () => {
+// await Promise.all([
+// page.goto('http://localhost:6969/routes-and-params/hrefs?relative=1'),
+// page.waitForNavigation(),
+// ]);
+// const url = await page.url();
+// expect(url).toEqual('http://localhost:6969/routes-and-params/relative');
+// });
+
+// test('ssr relativeComposed', async () => {
+// await Promise.all([
+// page.goto('http://localhost:6969/routes-and-params/hrefs?relativeComposed=1'),
+// page.waitForNavigation(),
+// ]);
+// const url = await page.url();
+// expect(url).toEqual('http://localhost:6969/routes-and-params/relative/6969');
+// });
+
+// test('ssr root', async () => {
+// await Promise.all([
+// page.goto('http://localhost:6969/routes-and-params/hrefs?root=1'),
+// page.waitForNavigation(),
+// ]);
+// const url = await page.url();
+// expect(url).toEqual('http://localhost:6969/root/6969');
+// });
+
+// });
diff --git a/tests/src/ServerFunctions.njs b/tests/src/ServerFunctions.njs
index c9d391f0..325817a5 100644
--- a/tests/src/ServerFunctions.njs
+++ b/tests/src/ServerFunctions.njs
@@ -63,10 +63,15 @@ class ServerFunctions extends Nullstack {
return true
}
+ static async getRequestUrl({ request }) {
+ return request.originalUrl.startsWith('/')
+ }
+
async initiate() {
this.statement = await this.useNodeFileSystem();
this.response = await this.useFetchInNode();
this.doublePlusOneServer = await ServerFunctions.getDoublePlusOne({ number: 34 })
+ this.originalUrl = await ServerFunctions.getRequestUrl()
}
async hydrate() {
@@ -74,11 +79,12 @@ class ServerFunctions extends Nullstack {
this.clientOnly = clientOnly();
this.doublePlusOneClient = await ServerFunctions.getDoublePlusOne({ number: 34 })
this.acceptsSpecialCharacters = await this.getEncodedString({ string: decodedString })
+ this.hydratedOriginalUrl = await ServerFunctions.getRequestUrl()
}
render() {
return (
-
+
@@ -91,6 +97,8 @@ class ServerFunctions extends Nullstack {
+
+
)
}
diff --git a/tests/src/ServerFunctions.test.js b/tests/src/ServerFunctions.test.js
index 14608f57..4e40f30c 100644
--- a/tests/src/ServerFunctions.test.js
+++ b/tests/src/ServerFunctions.test.js
@@ -1,10 +1,12 @@
-beforeAll(async () => {
- await page.goto('http://localhost:6969/server-functions');
-});
-
describe('ServerFunctions', () => {
+ beforeEach(async () => {
+ await page.goto('http://localhost:6969/server-functions');
+ });
+
+
test('instance can use returned values', async () => {
+ await page.waitForSelector('[data-hydrated]')
await page.click('.set-count-to-one');
await page.waitForSelector('[data-count="1"]');
const element = await page.$('[data-count="1"]');
@@ -12,6 +14,7 @@ describe('ServerFunctions', () => {
});
test('server functions accept an object as argument', async () => {
+ await page.waitForSelector('[data-hydrated]')
await page.click('.set-count-to-two');
await page.waitForSelector('[data-count="2"]');
const element = await page.$('[data-count="2"]');
@@ -19,6 +22,7 @@ describe('ServerFunctions', () => {
});
test('server functions serialize and deserialize dates', async () => {
+ await page.waitForSelector('[data-hydrated]')
await page.click('.set-date');
await page.waitForSelector('[data-year="1992"]');
const element = await page.$('[data-year="1992"]');
@@ -66,4 +70,16 @@ describe('ServerFunctions', () => {
expect(element).toBeTruthy();
});
+ test('static server functions receive the context on spa', async () => {
+ await page.waitForSelector('[data-hydrated-original-url]');
+ const element = await page.$('[data-hydrated-original-url]');
+ expect(element).toBeTruthy();
+ });
+
+ test('static server functions receive the context on ssr', async () => {
+ await page.waitForSelector('[data-original-url]');
+ const element = await page.$('[data-original-url]');
+ expect(element).toBeTruthy();
+ });
+
});
\ No newline at end of file
diff --git a/tests/src/ServerRequestAndResponse.njs b/tests/src/ServerRequestAndResponse.njs
index 1cdf7c39..302b2d55 100644
--- a/tests/src/ServerRequestAndResponse.njs
+++ b/tests/src/ServerRequestAndResponse.njs
@@ -19,12 +19,15 @@ class ServerRequestAndResponse extends Nullstack {
const response = await fetch('/api', { method, body });
responses[`data-${method.toLowerCase()}`] = response.status === 200;
}
+ const response = await fetch('/custom-api-before-start')
+ const data = await response.json()
+ this.startValue = data.startValue
this.responses = responses;
}
render() {
return (
-
+
)
}
diff --git a/tests/src/ServerRequestandResponse.test.js b/tests/src/ServerRequestandResponse.test.js
index 40d598f1..ffd1bf4e 100644
--- a/tests/src/ServerRequestandResponse.test.js
+++ b/tests/src/ServerRequestandResponse.test.js
@@ -5,13 +5,13 @@ beforeAll(async () => {
describe('ServerRequestAndResponse', () => {
test('the port key makes server run in the defined port', async () => {
- const response = await page.goto('http://localhost:6969');
+ const response = await page.goto('http://localhost:6969', { waitUntil: "networkidle0" });
const status = response.status();
expect([200, 304]).toContain(status);
});
test('server accepts use of middlewares', async () => {
- const response = await page.goto('http://localhost:6969/api');
+ const response = await page.goto('http://localhost:6969/api', { waitUntil: "networkidle0" });
const status = response.status();
expect([200, 304]).toContain(status);
});
@@ -56,4 +56,9 @@ describe('ServerRequestAndResponse', () => {
expect(element).toBeFalsy();
});
+ test('server middlewares run context start', async () => {
+ const element = await page.$('[data-start-value]');
+ expect(element).toBeFalsy();
+ });
+
});
\ No newline at end of file
diff --git a/tests/src/StatefulComponent.njs b/tests/src/StatefulComponent.njs
index 988c625f..4b3eb6c3 100644
--- a/tests/src/StatefulComponent.njs
+++ b/tests/src/StatefulComponent.njs
@@ -21,11 +21,24 @@ class StatefulComponent extends Nullstack {
this.count++;
}
- render({ self }) {
+ render() {
return (
-
)
}
diff --git a/tests/src/StatefulComponent.test.js b/tests/src/StatefulComponent.test.js
index d39d4802..ab390015 100644
--- a/tests/src/StatefulComponent.test.js
+++ b/tests/src/StatefulComponent.test.js
@@ -50,7 +50,7 @@ describe('StatefulComponent', () => {
test('empty strings generate nodes', async () => {
await page.click('[data-fill]');
await page.waitForSelector('[data-empty="not"]');
- const text = await page.$eval('[data-empty="not"]', (e) => e.innerText);
+ const text = await page.$eval('[data-empty="not"]', (e) => e.textContent);
expect(text).toMatch('not');
});
@@ -64,4 +64,35 @@ describe('StatefulComponent', () => {
expect(hasConsoleError).toBeFalsy();
});
+ test('textareas with multiple nodes become a single node', async () => {
+ const text = await page.$eval('textarea', (e) => e.value);
+ expect(text).toMatch(' 1 1 ');
+ });
+
+ test('textareas with multiple nodes can be updated', async () => {
+ await page.click('.increment-by-one');
+ await page.waitForSelector('[data-count="2"]');
+ const text = await page.$eval('textarea', (e) => e.value);
+ expect(text).toMatch(' 2 2 ');
+ });
+
+ test('children of style become the tags html attribute', async () => {
+ await page.click('.increment-by-one');
+ await page.waitForSelector('[data-count="2"]');
+ const text = await page.$eval('button', (e) => getComputedStyle(e).backgroundColor);
+ expect(text).toMatch('rgba(0, 0, 0, 0.2)');
+ });
+
+ test('attributes can prerender a zero', async () => {
+ await page.waitForSelector('[data-zero="0"]');
+ const element = await page.$('[data-zero="0"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('attributes can rererender a zero', async () => {
+ await page.waitForSelector('[data-hydrated-zero="0"]');
+ const element = await page.$('[data-hydrated-zero="0"]');
+ expect(element).toBeTruthy();
+ });
+
});
\ No newline at end of file
diff --git a/tests/src/TextObserver.njs b/tests/src/TextObserver.njs
new file mode 100644
index 00000000..91f8e061
--- /dev/null
+++ b/tests/src/TextObserver.njs
@@ -0,0 +1,38 @@
+import Nullstack from 'nullstack';
+
+class TextObserver extends Nullstack {
+
+ unmutatedText = 'default'
+ mutatedText = 'default'
+
+ hydrate() {
+ const config = {
+ characterData: true,
+ childList: true,
+ subtree: true,
+ };
+ const observer = new MutationObserver((mutationsList, observer) => {
+ for (const mutation of mutationsList) {
+ mutation.target.parentElement.dataset.mutated = true
+ }
+ observer.disconnect();
+ });
+ for (const element of [...document.querySelectorAll('[data-text-observer]')]) {
+ observer.observe(element, config);
+ }
+ this.mutatedText = 'mutated'
+ }
+
+ render() {
+ return (
+
+ regular text
+ {this.unmutatedText}
+ {this.mutatedText}
+
+ )
+ }
+
+}
+
+export default TextObserver;
\ No newline at end of file
diff --git a/tests/src/TextObserver.test.js b/tests/src/TextObserver.test.js
new file mode 100644
index 00000000..e2f0f31f
--- /dev/null
+++ b/tests/src/TextObserver.test.js
@@ -0,0 +1,23 @@
+beforeAll(async () => {
+ await page.goto('http://localhost:6969/text-observer');
+ await page.waitForSelector('[data-mutated]')
+});
+
+describe('TextObserver', () => {
+
+ test('regular text should never be mutated', async () => {
+ const element = await page.$('[data-regular-text]:not([data-mutated])');
+ expect(element).toBeTruthy();
+ });
+
+ test('unmutated text should not be mutated', async () => {
+ const element = await page.$('[data-unmutated-text]:not([data-mutated])');
+ expect(element).toBeTruthy();
+ });
+
+ test('mutated text should be mutated and redundancy should be redundant', async () => {
+ const element = await page.$('[data-mutated-text][data-mutated]');
+ expect(element).toBeTruthy();
+ });
+
+});
\ No newline at end of file
diff --git a/tests/src/TwoWayBindings.njs b/tests/src/TwoWayBindings.njs
index 7875cf08..b95e60f0 100644
--- a/tests/src/TwoWayBindings.njs
+++ b/tests/src/TwoWayBindings.njs
@@ -1,4 +1,5 @@
import Nullstack from 'nullstack';
+import TwoWayBindingsExternalComponent from './TwoWayBindingsExternalComponent';
class TwoWayBindings extends Nullstack {
@@ -11,48 +12,94 @@ class TwoWayBindings extends Nullstack {
object = { count: 1 };
array = ['a', 'b', 'c'];
- parse({ event, onchange }) {
+ byKeyName = 'byKeyNameValue'
+ keyName = 'byKeyName'
+
+ zero = 0
+
+ bringsHappiness = false
+
+ external = 'external'
+
+ debouncedBind = '69'
+ debouncedObject = '69'
+ debouncedEvent = '69'
+ debounceTime = 1000
+
+ parse({ event, source: bind, callback }) {
const normalized = event.target.value.replace(',', '').padStart(3, '0');
const whole = (parseInt(normalized.slice(0, -2)) || 0).toString();
const decimal = normalized.slice(normalized.length - 2);
const value = parseFloat(whole + '.' + decimal);
- const bringsHappyness = value >= 1000000;
- onchange({ value, bringsHappyness });
+ const bringsHappiness = value >= 1000000;
+ bind.object[bind.property] = value
+ callback({ bringsHappiness })
}
- renderCurrencyInput({ value, name }) {
- const formatted = value.toFixed(2).replace('.', ',');
- return
+ renderCurrencyInput({ bind, onchange }) {
+ const formatted = bind.object[bind.property].toFixed(2).replace('.', ',');
+ return
+ }
+
+ renderBubble({ bind }) {
+ return (
+
+ )
}
updateCharacter({ value }) {
this.character = this.array[value];
}
+ setHappiness({ bringsHappiness }) {
+ this.bringsHappiness = bringsHappiness
+ }
+
+ debouncedEventHandler({ event }) {
+ if (event.type === 'click') {
+ this.debouncedEvent = '6969'
+ }
+ }
+
render({ params }) {
return (
-
+
)
}
diff --git a/tests/src/TwoWayBindings.test.js b/tests/src/TwoWayBindings.test.js
index 10bb636a..d46091d8 100644
--- a/tests/src/TwoWayBindings.test.js
+++ b/tests/src/TwoWayBindings.test.js
@@ -1,8 +1,13 @@
-beforeAll(async () => {
- await page.goto('http://localhost:6969/two-way-bindings?page=1');
-});
+describe('TwoWayBindings', () => {
-describe('ContextPage', () => {
+ beforeEach(async () => {
+ await page.goto('http://localhost:6969/two-way-bindings?page=1');
+ });
+
+ test('inputs can be bound to zero', async () => {
+ const element = await page.$('[value="0"]');
+ expect(element).toBeTruthy();
+ });
test('bind adds a name attribute to the element', async () => {
const element = await page.$('[name="number"]');
@@ -24,6 +29,27 @@ describe('ContextPage', () => {
expect(value).toMatch('aaaa');
});
+ test('textareas value reflects variable changes', async () => {
+ await page.click('[data-textarea]')
+ await page.waitForSelector('textarea[data-text="bbbb"]')
+ const value = await page.$eval('[name="text"]', (element) => element.value);
+ expect(value).toMatch('bbbb');
+ });
+
+ test('checkboxes value reflects variable changes', async () => {
+ await page.click('[data-checkbox]')
+ await page.waitForSelector('input[type=checkbox]:not(:checked)')
+ const element = await page.$('input[type=checkbox]:not(:checked)')
+ expect(element).toBeTruthy();
+ });
+
+ test('select value reflects variable changes', async () => {
+ await page.click('[data-select]')
+ await page.waitForSelector('[data-character="b"]')
+ const value = await page.$eval('select', (element) => element.value);
+ expect(value).toMatch('b');
+ });
+
test('selects can be bound', async () => {
const value = await page.$eval('[name="character"]', (element) => element.value);
expect(value).toMatch('a');
@@ -50,7 +76,7 @@ describe('ContextPage', () => {
});
test('custom inputs can be bound', async () => {
- const value = await page.$eval('[name="currency"]', (element) => element.value);
+ const value = await page.$eval('[data-currency]', (element) => element.value);
expect(value).toMatch('100,00');
});
@@ -73,17 +99,121 @@ describe('ContextPage', () => {
});
test('bind keeps the primitive type of the variable', async () => {
+ await page.type('[name="number"]', '2');
await page.waitForSelector('[data-number-type="number"]');
const element = await page.$('[data-number-type="number"]');
expect(element).toBeTruthy();
});
test('bound inputs can have custom events that triger after the value is set', async () => {
+ await page.type('[name="number"]', '2');
await page.waitForSelector('[data-character="b"]');
const element = await page.$('[data-character="b"]');
expect(element).toBeTruthy();
});
- // test extra params bringHappiness
+ test('developers can create custom bindable components', async () => {
+ await page.type('[data-currency]', '696969696969');
+ await page.waitForSelector('[data-brings-happiness]')
+ const element = await page.$('[data-brings-happiness]');
+ expect(element).toBeTruthy();
+ });
+
+ test('bind should accept composed computed properties', async () => {
+ const value = await page.$eval('[name="composedComputed"]', (element) => element.value);
+ expect(value).toMatch('byKeyNameValue');
+ });
+
+ test('bind should accept logical computed properties', async () => {
+ const value = await page.$eval('[name="logicalComputed"]', (element) => element.value);
+ expect(value).toMatch('byKeyNameValue');
+ });
+
+ test('bind should accept literal computed properties', async () => {
+ const value = await page.$eval('[name="literalComputed"]', (element) => element.value);
+ expect(value).toMatch('byKeyNameValue');
+ });
+
+ test('bind can be bubbled down', async () => {
+ const value = await page.$eval('[name="bubble"]', (element) => element.value);
+ expect(value).toMatch('byKeyNameValue');
+ });
+
+ test('bind should prerender in external components', async () => {
+ const value = await page.$eval('[name="externalComponent"]', (element) => element.value);
+ expect(value).toMatch('external');
+ });
+
+ test('bind should rerender in external components', async () => {
+ await page.type('[data-value]', 'new');
+ await page.waitForSelector('[data-value="external"]')
+ const value = await page.$eval('[name="externalComponent"]', (element) => element.value);
+ expect(value).toMatch('newexternal');
+ });
+
+ test('bind can be debounced', async () => {
+ await page.type('[data-debounced-bind]', '69');
+ await page.waitForTimeout(1000)
+ const originalValue = await page.$('[data-debounced-bind="69"]');
+ await page.waitForTimeout(1500)
+ const updatedValue = await page.$('[data-debounced-bind="6969"]');
+ expect(originalValue && updatedValue).toBeTruthy();
+ });
+
+ test('object events can be debounced', async () => {
+ await page.click('[data-debounced-object]');
+ await page.waitForTimeout(1000)
+ const originalValue = await page.$('[data-debounced-object="69"]');
+ await page.waitForTimeout(1500)
+ const updatedValue = await page.$('[data-debounced-object="6969"]');
+ expect(originalValue && updatedValue).toBeTruthy();
+ });
+
+ test('events can be debounced', async () => {
+ await page.click('[data-debounced-event]');
+ await page.waitForTimeout(1000)
+ const originalValue = await page.$('[data-debounced-event="69"]');
+ await page.waitForTimeout(1500)
+ const updatedValue = await page.$('[data-debounced-event="6969"]');
+ expect(originalValue && updatedValue).toBeTruthy();
+ });
+
+ test('debounced events keep the reference to the original event', async () => {
+ await page.click('[data-debounced-event]');
+ await page.waitForTimeout(1000)
+ const originalValue = await page.$('[data-debounced-event="69"]');
+ await page.waitForTimeout(1500)
+ const updatedValue = await page.$('[data-debounced-event="6969"]');
+ expect(originalValue && updatedValue).toBeTruthy();
+ });
+
+ test('debounce attribute should not be present in dom during prerender', async () => {
+ const element = await page.$('[data-debounced-event]:not([debounce])');
+ expect(element).toBeTruthy();
+ });
+
+ test('debounce attribute should not be present in dom during render', async () => {
+ await page.waitForSelector('[data-debounced-hydrated]')
+ const element = await page.$('[data-debounced-hydrated]:not([debounce])');
+ expect(element).toBeTruthy();
+ });
+
+ test('debounce attribute should not be present in dom during rerender', async () => {
+ await page.click('[data-debounced-rerender]:not([debounce])');
+ await page.waitForSelector('[data-debounced-rerender="2000"]:not([debounce])')
+ const element = await page.$('[data-debounced-rerender="2000"]:not([debounce])');
+ expect(element).toBeTruthy();
+ });
+
+ test('custom bindable components receive a no op function if no onchange is passed to them', async () => {
+ await page.waitForSelector('[data-onchange="noop"]')
+ const element = await page.$('[data-onchange="noop"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('binding to nested undefined sets the value to an empty string', async () => {
+ const value = await page.$eval('[data-undeclared-nested]', (element) => element.value);
+ expect(value).toMatch('');
+ });
});
\ No newline at end of file
diff --git a/tests/src/TwoWayBindingsExternalComponent.njs b/tests/src/TwoWayBindingsExternalComponent.njs
new file mode 100644
index 00000000..2f4d85fb
--- /dev/null
+++ b/tests/src/TwoWayBindingsExternalComponent.njs
@@ -0,0 +1,15 @@
+import Nullstack from 'nullstack';
+
+class TwoWayBindingsExternalComponent extends Nullstack {
+
+ render({ bind, onchange }) {
+ return (
+
+
+
+ )
+ }
+
+}
+
+export default TwoWayBindingsExternalComponent;
\ No newline at end of file
diff --git a/tests/src/TypeScript.nts b/tests/src/TypeScript.nts
index 1f0cc82b..454ce758 100644
--- a/tests/src/TypeScript.nts
+++ b/tests/src/TypeScript.nts
@@ -17,12 +17,12 @@ class TypeScript extends Nullstack {
return
}
- render({ self }: NullstackClientContext) {
+ render() {
return (
diff --git a/tests/src/TypesScript.test.js b/tests/src/TypesScript.test.js
index 0db7e1e1..235c421b 100644
--- a/tests/src/TypesScript.test.js
+++ b/tests/src/TypesScript.test.js
@@ -24,8 +24,8 @@ describe('UnderscoredAttributes', () => {
});
test('bind should work on nts files', async () => {
- await page.waitForSelector('[bind="count"]');
- const element = await page.$('[bind="count"]');
+ await page.waitForSelector('input');
+ const element = await page.$('input');
expect(element).toBeTruthy();
});
diff --git a/tests/src/UndefinedNodes.test.js b/tests/src/UndefinedNodes.test.js
index d6caed79..3780b59e 100644
--- a/tests/src/UndefinedNodes.test.js
+++ b/tests/src/UndefinedNodes.test.js
@@ -10,7 +10,7 @@ describe('UndefinedNodes WithoutReturn', () => {
describe('UndefinedNodes WithoutUndefinedReturn', () => {
test('renderable functions with undefined return should raise an error', async () => {
- const response = await page.goto('http://localhost:6969/undefined-nodes?withoutUndefinedReturn=true');
+ const response = await page.goto('http://localhost:6969/undefined-nodes?withoutUndefinedReturn=true', { waitUntil: "networkidle0" });
expect(response.status()).toEqual(500)
});
@@ -19,7 +19,7 @@ describe('UndefinedNodes WithoutUndefinedReturn', () => {
describe('UndefinedNodes WithoutRetunr', () => {
test('tagging a renderable function that does not exist should raise an error', async () => {
- const response = await page.goto('http://localhost:6969/undefined-nodes?withoutRetunr=true');
+ const response = await page.goto('http://localhost:6969/undefined-nodes?withoutRetunr=true', { waitUntil: "networkidle0" });
expect(response.status()).toEqual(500)
});
@@ -28,7 +28,7 @@ describe('UndefinedNodes WithoutRetunr', () => {
describe('UndefinedNodes ForgotToImport', () => {
test('tagging a renderable function that was not imported should raise an error', async () => {
- const response = await page.goto('http://localhost:6969/undefined-nodes?forgotToImport=true');
+ const response = await page.goto('http://localhost:6969/undefined-nodes?forgotToImport=true', { waitUntil: "networkidle0" });
expect(response.status()).toEqual(500)
});
diff --git a/tests/src/UnderscoredAttributes.njs b/tests/src/UnderscoredAttributes.njs
index 2b910007..70e1ec5a 100644
--- a/tests/src/UnderscoredAttributes.njs
+++ b/tests/src/UnderscoredAttributes.njs
@@ -10,9 +10,9 @@ const underscoredObject = {
}
}
-function _underscored(value) {
- this.e = value
- }
+function _underscored({ string, value }) {
+ this.e = string === 'nullstack' && value
+}
class UnderscoredAttributes extends Nullstack {
@@ -21,6 +21,11 @@ class UnderscoredAttributes extends Nullstack {
c = 0
d = 0
e = 0
+ f = 0
+
+ prepare() {
+ this._g = 1
+ }
_underscoredMethod(value) {
this.a = value
@@ -32,6 +37,10 @@ class UnderscoredAttributes extends Nullstack {
notUnderscored = _underscored
+ _underscoredEvent() {
+ this.f = 1
+ }
+
hydrate() {
this._underscoredMethod(1)
this._underscoredAttributeFunction(1)
@@ -39,13 +48,17 @@ class UnderscoredAttributes extends Nullstack {
this._underscoredAfterConstructor(1)
this._underscoredObject = underscoredObject
this._underscoredObject.withoutUnderscore(1)
- this.notUnderscored(1)
+ this.notUnderscored({ value: 1 })
+ }
+
+ setEventListener({ element }) {
+ element.addEventListener('click', this._underscoredEvent)
}
render() {
return (
-
-
UnderscoredAttributes
+
+
)
}
diff --git a/tests/src/UnderscoredAttributes.test.js b/tests/src/UnderscoredAttributes.test.js
index 52ad1f92..aec0bd21 100644
--- a/tests/src/UnderscoredAttributes.test.js
+++ b/tests/src/UnderscoredAttributes.test.js
@@ -31,9 +31,22 @@ describe('UnderscoredAttributes', () => {
expect(element).toBeTruthy();
});
- test('keys assigned with a function that name is underscored do not receive the context as argument', async () => {
+ test('keys assigned with a function that name is underscored receive the context as argument', async () => {
const element = await page.$('[data-e="1"]');
expect(element).toBeTruthy();
});
+ test('this is bound to the instance on underscored functions even on events', async () => {
+ await page.waitForSelector('[data-hydrated]')
+ await page.click('[data-hydrated]')
+ await page.waitForSelector('[data-f="1"]')
+ const element = await page.$('[data-f="1"]');
+ expect(element).toBeTruthy();
+ });
+
+ test('underscored variables are serialized for hydration', async () => {
+ const element = await page.$('[data-g="1"]');
+ expect(element).toBeTruthy();
+ });
+
});
\ No newline at end of file
diff --git a/tests/src/scripts/run.js b/tests/src/scripts/run.js
index ecd99a81..97928d5d 100644
--- a/tests/src/scripts/run.js
+++ b/tests/src/scripts/run.js
@@ -1,6 +1,6 @@
process.env.NULLSTACK_ENVIRONMENT_NAME = 'test'
-const { default: application } = require('../../.development/server.js');
+const { default: application } = require('../../.production/server.js');
async function getProjectName() {
await application.start();
diff --git a/types/ClientContext.d.ts b/types/ClientContext.d.ts
index b48ac3bd..f41fbb98 100644
--- a/types/ClientContext.d.ts
+++ b/types/ClientContext.d.ts
@@ -3,7 +3,6 @@ import { NullstackPage } from "./Page";
import { NullstackParams } from "./Params";
import { NullstackProject } from "./Project";
import { NullstackRouter } from "./Router";
-import { NullstackSelf } from "./Self";
import { NullstackSettings } from "./Settings";
import { NullstackWorker } from "./Worker";
@@ -33,14 +32,6 @@ export type NullstackClientContext = {
*/
worker?: NullstackWorker;
- /**
- * It gives you information about the instance lifecycle and it's unique key.
- *
- * @see https://nullstack.app/instance-self
- * @see https://nullstack.app/instance-self#instance-key
- */
- self?: NullstackSelf;
-
/**
* It gives you information about the element dataset.
* Any `data-*` attributes will receive a respective camelized key on this object.
diff --git a/types/Environment.d.ts b/types/Environment.d.ts
index d38b5b7f..f81fd885 100644
--- a/types/Environment.d.ts
+++ b/types/Environment.d.ts
@@ -17,4 +17,12 @@ export type NullstackEnvironment = {
*/
key: string;
+ /**
+ * Event raised when a module is hot replaced.
+ *
+ * @scope client
+ * @see https://nullstack.app/context-environment
+ */
+ event: string;
+
};
\ No newline at end of file
diff --git a/types/JSX.d.ts b/types/JSX.d.ts
index 9e7b4461..429dd2a3 100644
--- a/types/JSX.d.ts
+++ b/types/JSX.d.ts
@@ -48,6 +48,8 @@ export interface Attributes {
html?: string | undefined;
source?: object | undefined;
bind?: any | undefined;
+ debounce?: number | undefined;
+ ref?: any | undefined;
data?: object | undefined;
"data-"?: any;
[key: string]: any;
@@ -60,7 +62,7 @@ export interface NullstackAttributes extends Attributes {
key?: string;
}
-export interface ClassAttributes extends Attributes {}
+export interface ClassAttributes extends Attributes { }
//
// Factories
@@ -68,7 +70,7 @@ export interface ClassAttributes extends Attributes {}
type DetailedHTMLFactory
= P;
-export interface SVGFactory {}
+export interface SVGFactory { }
export type NullstackFragment = NullstackNode[];
export type NullstackNode =
@@ -107,7 +109,7 @@ export interface BaseSyntheticEvent {
* If you thought this should be `EventTarget & T`, see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/11508#issuecomment-256045682
*/
export interface SyntheticEvent
- extends BaseSyntheticEvent {}
+ extends BaseSyntheticEvent { }
export interface DragEvent extends MouseEvent {
dataTransfer: DataTransfer;
@@ -133,7 +135,7 @@ export interface FocusEvent
target: EventTarget & Target;
}
-export interface FormEvent extends SyntheticEvent {}
+export interface FormEvent extends SyntheticEvent { }
export interface ChangeEvent extends SyntheticEvent {
target: EventTarget & T;
@@ -207,8 +209,8 @@ export interface WheelEvent
type EventHandler> =
| object
| {
- bivarianceHack(event: { event: E } & NullstackClientContext): void;
- }["bivarianceHack"];
+ bivarianceHack(event: { event: E } & NullstackClientContext): void;
+ }["bivarianceHack"];
type NullstackEventHandler = EventHandler>;
type DragEventHandler = EventHandler>;
@@ -227,7 +229,7 @@ type WheelEventHandler = EventHandler>;
type DetailedHTMLProps, T> = E;
-export interface SVGProps extends SVGAttributes, ClassAttributes {}
+export interface SVGProps extends SVGAttributes, ClassAttributes { }
export interface DOMAttributes extends Attributes {
// Focus Events
@@ -354,15 +356,15 @@ export interface AriaAttributes {
"aria-controls"?: string | undefined;
/** Indicates the element that represents the current item within a container or set of related elements. */
"aria-current"?:
- | boolean
- | "false"
- | "true"
- | "page"
- | "step"
- | "location"
- | "date"
- | "time"
- | undefined;
+ | boolean
+ | "false"
+ | "true"
+ | "page"
+ | "step"
+ | "location"
+ | "date"
+ | "time"
+ | undefined;
/**
* Identifies the element (or elements) that describes the object.
* @see aria-labelledby
@@ -383,13 +385,13 @@ export interface AriaAttributes {
* @deprecated in ARIA 1.1
*/
"aria-dropeffect"?:
- | "none"
- | "copy"
- | "execute"
- | "link"
- | "move"
- | "popup"
- | undefined;
+ | "none"
+ | "copy"
+ | "execute"
+ | "link"
+ | "move"
+ | "popup"
+ | undefined;
/**
* Identifies the element that provides an error message for the object.
* @see aria-invalid @see aria-describedby.
@@ -409,15 +411,15 @@ export interface AriaAttributes {
"aria-grabbed"?: Booleanish | undefined;
/** Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. */
"aria-haspopup"?:
- | boolean
- | "false"
- | "true"
- | "menu"
- | "listbox"
- | "tree"
- | "grid"
- | "dialog"
- | undefined;
+ | boolean
+ | "false"
+ | "true"
+ | "menu"
+ | "listbox"
+ | "tree"
+ | "grid"
+ | "dialog"
+ | undefined;
/**
* Indicates whether the element is exposed to an accessibility API.
* @see aria-disabled.
@@ -428,12 +430,12 @@ export interface AriaAttributes {
* @see aria-errormessage.
*/
"aria-invalid"?:
- | boolean
- | "false"
- | "true"
- | "grammar"
- | "spelling"
- | undefined;
+ | boolean
+ | "false"
+ | "true"
+ | "grammar"
+ | "spelling"
+ | undefined;
/** Indicates keyboard shortcuts that an author has implemented to activate or give focus to an element. */
"aria-keyshortcuts"?: string | undefined;
/**
@@ -489,17 +491,17 @@ export interface AriaAttributes {
* @see aria-atomic.
*/
"aria-relevant"?:
- | "additions"
- | "additions removals"
- | "additions text"
- | "all"
- | "removals"
- | "removals additions"
- | "removals text"
- | "text"
- | "text additions"
- | "text removals"
- | undefined;
+ | "additions"
+ | "additions removals"
+ | "additions text"
+ | "all"
+ | "removals"
+ | "removals additions"
+ | "removals text"
+ | "text"
+ | "text additions"
+ | "text removals"
+ | undefined;
/** Indicates that user input is required on the element before a form may be submitted. */
"aria-required"?: Booleanish | undefined;
/** Defines a human-readable, author-localized description for the role of an element. */
@@ -631,7 +633,7 @@ export interface HTMLAttributes extends AriaAttributes, DOMAttributes {
placeholder?: string | undefined;
slot?: string | undefined;
spellcheck?: Booleanish | undefined;
- style?: object | undefined;
+ style?: string | undefined;
tabindex?: number | string | undefined;
title?: string | undefined;
translate?: "yes" | "no" | undefined;
@@ -653,15 +655,15 @@ export interface HTMLAttributes extends AriaAttributes, DOMAttributes {
* @see https://html.spec.whatwg.org/multipage/interactiohtml#input-modalities:-the-inputmode-attribute
*/
inputmode?:
- | "none"
- | "text"
- | "tel"
- | "url"
- | "email"
- | "numeric"
- | "decimal"
- | "search"
- | undefined;
+ | "none"
+ | "text"
+ | "tel"
+ | "url"
+ | "email"
+ | "numeric"
+ | "decimal"
+ | "search"
+ | undefined;
/**
* Specify that a standard HTML element should behave like a defined custom built-in element
* @see https://html.spec.whatwg.org/multipage/custom-elements.html#attr-is
@@ -801,7 +803,7 @@ export interface AnchorHTMLAttributes extends HTMLAttributes {
path?: string | undefined;
}
-export interface AudioHTMLAttributes extends MediaHTMLAttributes {}
+export interface AudioHTMLAttributes extends MediaHTMLAttributes { }
export interface AreaHTMLAttributes extends HTMLAttributes {
alt?: string | undefined;
@@ -977,14 +979,14 @@ export interface InputHTMLAttributes extends HTMLAttributes {
checked?: boolean | undefined;
disabled?: boolean | undefined;
enterkeyhint?:
- | "enter"
- | "done"
- | "go"
- | "next"
- | "previous"
- | "search"
- | "send"
- | undefined;
+ | "enter"
+ | "done"
+ | "go"
+ | "next"
+ | "previous"
+ | "search"
+ | "send"
+ | undefined;
form?: string | undefined;
formaction?: string | undefined;
formenctype?: string | undefined;
@@ -1260,7 +1262,7 @@ export interface SVGAttributes extends AriaAttributes, DOMAttributes {
method?: string | undefined;
min?: number | string | undefined;
name?: string | undefined;
- style?: object | undefined;
+ style?: string | undefined;
target?: string | undefined;
type?: string | undefined;
width?: number | string | undefined;
@@ -1304,8 +1306,8 @@ declare global {
namespace JSX {
type Element = NullstackNode;
- interface IntrinsicAttributes extends NullstackAttributes {}
- interface IntrinsicClassAttributes extends ClassAttributes {}
+ interface IntrinsicAttributes extends NullstackAttributes { }
+ interface IntrinsicClassAttributes extends ClassAttributes { }
interface AllElements {
// HTML
@@ -1664,6 +1666,6 @@ declare global {
element: ElementTagHTMLAttributes;
}
- interface IntrinsicElements extends ExoticElements, AllElements {}
+ interface IntrinsicElements extends ExoticElements, AllElements { }
}
}
diff --git a/types/Self.d.ts b/types/Self.d.ts
deleted file mode 100644
index a6fe2510..00000000
--- a/types/Self.d.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-export type NullstackSelf = {
-
- initiated: boolean;
-
- hydrated: boolean;
-
- terminated: boolean;
-
- prerendered: boolean;
-
- /**
- * If the component is persistent
- *
- * @see https://nullstack.app/persistent-components
- */
- persistent: boolean,
-
- /**
- * Only available after hydration
- */
- element?: HTMLElement;
-
- key: string;
-
-};
\ No newline at end of file
diff --git a/types/index.d.ts b/types/index.d.ts
index b1081dbf..f490fa0b 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -4,7 +4,7 @@ import { NullstackServerContext } from "./ServerContext";
export interface NullstackContext
extends NullstackClientContext,
- NullstackServerContext {}
+ NullstackServerContext { }
export * from "./ClientContext";
export * from "./Environment";
@@ -14,7 +14,6 @@ export * from "./Plugin";
export * from "./Project";
export * from "./Router";
export * from "./Secrets";
-export * from "./Self";
export * from "./Server";
export * from "./ServerContext";
export * from "./Settings";
@@ -28,47 +27,64 @@ export default class Nullstack {
* @param App A Nullstack app root component
*/
static start?(App: any): NullstackContext;
-
+
/**
* Use a plugin
*/
static use?(Plugin: NullstackPlugin): void;
-
+
/**
* Could run on the server or client.
* @see https://nullstack.app/full-stack-lifecycle#prepare
*/
prepare?(context: NullstackContext & TProps): void;
-
+
/**
* Could run on the server or client.
* @see https://nullstack.app/full-stack-lifecycle#initiate
*/
initiate?(context: NullstackContext & TProps): void;
-
+
+ initiated: boolean;
+
/**
* Could run on the server or client.
* @see https://nullstack.app/full-stack-lifecycle#launch
*/
launch?(context: NullstackContext & TProps): void;
-
+
/**
* Runs on the client.
* @see https://nullstack.app/full-stack-lifecycle#hydrate
*/
hydrate?(context: NullstackClientContext & TProps): void;
-
+
+ hydrated: boolean;
+
/**
* Runs on the client.
* @see https://nullstack.app/full-stack-lifecycle#update
*/
update?(context: NullstackContext & TProps): void;
-
+
/**
* Runs on the client.
* @see https://nullstack.app/full-stack-lifecycle#terminate
*/
terminate?(context: NullstackContext & TProps): void;
+ terminated: boolean;
+
render?(context: NullstackContext & TProps): void;
+
+ prerendered: boolean;
+
+ /**
+ * If the component is persistent
+ *
+ * @see https://nullstack.app/persistent-components
+ */
+ persistent: boolean;
+
+ key: string;
}
diff --git a/webpack.config.js b/webpack.config.js
index 07980c30..51797d67 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,12 +1,19 @@
const path = require('path');
-const NodemonPlugin = require('nodemon-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const TerserPlugin = require('terser-webpack-plugin');
const crypto = require("crypto");
const { readdirSync } = require('fs');
+const NodemonPlugin = require('nodemon-webpack-plugin');
+const CopyPlugin = require("copy-webpack-plugin");
const buildKey = crypto.randomBytes(20).toString('hex');
+customConsole = new Proxy({}, {
+ get() {
+ return () => { }
+ }
+})
+
function getLoader(loader) {
const loaders = path.resolve('./node_modules/nullstack/loaders');
return path.join(loaders, loader);
@@ -115,17 +122,21 @@ function server(env, argv) {
const folder = isDev ? '.development' : '.production';
const devtool = isDev ? 'inline-cheap-module-source-map' : false;
const minimize = !isDev;
- const plugins = isDev ? ([
- new NodemonPlugin({
+ const plugins = []
+ if (isDev) {
+ plugins.push(new NodemonPlugin({
ext: '*',
- watch: [".env", ".env.*", './.development/*.*'],
+ watch: [".env", ".env.*", './.development/server.js'],
script: './.development/server.js',
nodeArgs: ['--enable-source-maps'],
quiet: true
- })
- ]) : [];
+ }))
+ }
return {
mode: argv.environment,
+ infrastructureLogging: {
+ console: customConsole,
+ },
entry: './server.js',
output: {
path: path.join(dir, folder),
@@ -211,6 +222,10 @@ function server(env, argv) {
test: /\.(njs|nts|jsx|tsx)$/,
loader: getLoader('register-inner-components.js'),
},
+ {
+ test: /\.(njs|nts|jsx|tsx)$/,
+ loader: getLoader('transform-node-ref.js'),
+ },
{
issuer: /worker.js/,
resourceQuery: /raw/,
@@ -229,27 +244,29 @@ function server(env, argv) {
}
function client(env, argv) {
+ const disk = !!argv.disk
const dir = argv.input ? path.join(__dirname, argv.input) : process.cwd();
const isDev = argv.environment === 'development';
const folder = isDev ? '.development' : '.production';
const devtool = isDev ? 'inline-cheap-module-source-map' : false;
const minimize = !isDev;
- let liveReload = {};
- if (!isDev) {
- liveReload = {
- test: /liveReload.js$/,
- use: [
- { loader: getLoader('ignore-import.js') }
- ]
- }
- }
const plugins = [
new MiniCssExtractPlugin({
filename: "client.css",
chunkFilename: '[chunkhash].client.css'
- })
+ }),
]
+ if (disk) {
+ plugins.push(new CopyPlugin({
+ patterns: [
+ { from: "public", to: "../.development" },
+ ]
+ }))
+ }
return {
+ infrastructureLogging: {
+ console: customConsole,
+ },
mode: argv.environment,
entry: './client.js',
output: {
@@ -273,6 +290,10 @@ function client(env, argv) {
stats: 'errors-only',
module: {
rules: [
+ {
+ test: /client.js$/,
+ loader: getLoader('inject-hmr.js'),
+ },
{
test: /nullstack.js$/,
loader: getLoader('string-replace.js'),
@@ -307,7 +328,6 @@ function client(env, argv) {
{ loader: require.resolve('sass-loader') }
],
},
- liveReload,
nullstackTypescript,
{
test: /\.(njs|nts|jsx|tsx)$/,
@@ -317,6 +337,10 @@ function client(env, argv) {
test: /\.(njs|nts|jsx|tsx)$/,
loader: getLoader('register-inner-components.js'),
},
+ {
+ test: /\.(njs|nts|jsx|tsx)$/,
+ loader: getLoader('transform-node-ref.js'),
+ },
]
},
target: 'web',