coralite 0.40.2 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib/index.js CHANGED
@@ -3460,59 +3460,106 @@ var staticAssetPlugin = (assets = []) => {
3460
3460
  };
3461
3461
 
3462
3462
  // plugins/testing.js
3463
- function traverseAndAddTestId(children) {
3463
+ function traverseAndAddTestId(children, instanceId, { autoTestId = false, counters = {} } = {}) {
3464
3464
  if (!Array.isArray(children)) {
3465
3465
  return;
3466
3466
  }
3467
3467
  for (let i = 0; i < children.length; i++) {
3468
3468
  const node = children[i];
3469
- if (node.type === "tag" && node.attribs?.ref) {
3470
- if (!node.attribs["data-testid"]) {
3471
- node.attribs["data-testid"] = node.attribs.ref;
3469
+ if (node.type === "tag") {
3470
+ if (node.attribs?.ref) {
3471
+ if (!node.attribs["data-testid"]) {
3472
+ const refValue = node.attribs.ref;
3473
+ const prefix = instanceId ? `${instanceId}__` : "";
3474
+ if (instanceId === "page" && refValue.includes("__")) {
3475
+ node.attribs["data-testid"] = refValue;
3476
+ } else {
3477
+ node.attribs["data-testid"] = `${prefix}${refValue}`;
3478
+ }
3479
+ }
3480
+ } else if (autoTestId && instanceId) {
3481
+ const tagName = node.name.toLowerCase();
3482
+ const isInteractive = [
3483
+ "button",
3484
+ "a",
3485
+ "input",
3486
+ "form",
3487
+ "select",
3488
+ "textarea"
3489
+ ].includes(tagName) || node.attribs && (node.attribs.tabindex !== void 0 || node.attribs.role && ["button", "link", "checkbox"].includes(node.attribs.role)) || node.slots;
3490
+ if (isInteractive) {
3491
+ if (!counters[tagName]) {
3492
+ counters[tagName] = 0;
3493
+ }
3494
+ const index = counters[tagName]++;
3495
+ if (!node.attribs) {
3496
+ node.attribs = {};
3497
+ }
3498
+ if (!node.attribs["data-testid"]) {
3499
+ node.attribs["data-testid"] = `${instanceId}__${tagName}-${index}`;
3500
+ }
3501
+ }
3472
3502
  }
3473
3503
  }
3474
3504
  if (node.children?.length > 0) {
3475
- traverseAndAddTestId(node.children);
3505
+ traverseAndAddTestId(node.children, instanceId, {
3506
+ autoTestId,
3507
+ counters
3508
+ });
3476
3509
  }
3477
3510
  }
3478
3511
  }
3479
3512
  var testingPlugin = definePlugin({
3480
3513
  name: "testing",
3481
3514
  server: {
3482
- onBeforeComponentRender: ({ instanceId, refs }) => {
3483
- for (let i = 0; i < refs.length; i++) {
3484
- const ref = refs[i];
3485
- const uniqueRefValue = `${instanceId}__${ref.name}`;
3486
- if (ref.element.attribs) {
3487
- const currentTestId = ref.element.attribs["data-testid"];
3488
- if (!currentTestId || currentTestId === ref.name) {
3489
- ref.element.attribs["data-testid"] = uniqueRefValue;
3490
- }
3491
- }
3515
+ onBeforeBuild: ({ app }) => {
3516
+ if (app.options.mode !== "testing") {
3517
+ return;
3492
3518
  }
3519
+ app.options.externalStyles = app.options.externalStyles || [];
3520
+ const velocityStyle = `
3521
+ *, *::before, *::after {
3522
+ transition: none !important;
3523
+ animation: none !important;
3524
+ scroll-behavior: auto !important;
3525
+ }
3526
+ `.trim();
3527
+ app.options.externalStyles.push(`data:text/css;base64,${Buffer.from(velocityStyle).toString("base64")}`);
3493
3528
  },
3494
- onComponentSet: ({ component }) => {
3495
- const children = component?.template?.children;
3496
- if (children) {
3497
- traverseAndAddTestId(children);
3529
+ onBeforeComponentRender: ({ instanceId, template, app }) => {
3530
+ const isTesting = app.options.mode === "testing";
3531
+ const counters = {};
3532
+ const templateNode = template;
3533
+ if (templateNode && templateNode.children) {
3534
+ traverseAndAddTestId(templateNode.children, instanceId, {
3535
+ autoTestId: isTesting,
3536
+ counters
3537
+ });
3498
3538
  }
3499
3539
  },
3500
- onPageSet: ({ elements }) => {
3501
- const children = elements?.root?.children;
3502
- if (children) {
3503
- traverseAndAddTestId(children);
3504
- }
3540
+ onPageSet: ({ elements, app }) => {
3541
+ const isTesting = app.options.mode === "testing";
3542
+ const counters = {};
3543
+ traverseAndAddTestId(elements?.root?.children, "page", {
3544
+ autoTestId: isTesting,
3545
+ counters
3546
+ });
3505
3547
  }
3506
3548
  },
3507
3549
  client: {
3508
3550
  onBeforeComponentRender: ({ instanceId, refs }) => {
3509
3551
  for (let i = 0; i < refs.length; i++) {
3510
3552
  const ref = refs[i];
3511
- const uniqueRefValue = `${instanceId}__${ref.name}`;
3553
+ const prefix = `${instanceId}__`;
3554
+ const uniqueRefValue = ref.element && ref.element.getAttribute("ref");
3512
3555
  if (ref.element && ref.element.setAttribute) {
3513
3556
  const currentTestId = ref.element.getAttribute("data-testid");
3514
3557
  if (!currentTestId || currentTestId === ref.name) {
3515
- ref.element.setAttribute("data-testid", uniqueRefValue);
3558
+ if (uniqueRefValue && uniqueRefValue.startsWith(prefix)) {
3559
+ ref.element.setAttribute("data-testid", uniqueRefValue);
3560
+ } else {
3561
+ ref.element.setAttribute("data-testid", `${prefix}${ref.name}`);
3562
+ }
3516
3563
  }
3517
3564
  }
3518
3565
  }
@@ -4190,8 +4237,15 @@ function createComponentDefinition({ app }) {
4190
4237
  }
4191
4238
  }
4192
4239
  }
4193
- if (typeof server === "function") {
4194
- const serverResult = await server({
4240
+ let serverToExecute = server;
4241
+ if (app.options.mode === "testing" && app.options.testing?.mocks) {
4242
+ const mock = app.options.testing.mocks[module.id];
4243
+ if (mock && typeof mock.server === "function") {
4244
+ serverToExecute = mock.server;
4245
+ }
4246
+ }
4247
+ if (typeof serverToExecute === "function") {
4248
+ const serverResult = await serverToExecute({
4195
4249
  ...context2,
4196
4250
  ...initialState
4197
4251
  });
@@ -4609,8 +4663,8 @@ function createPageHandlers({
4609
4663
  data,
4610
4664
  app
4611
4665
  });
4612
- const isProduction = app.options.mode === "production";
4613
- if (isProduction && !data.virtual) {
4666
+ const isStatic = app.options.mode === "production" || app.options.mode === "testing";
4667
+ if (isStatic && !data.virtual) {
4614
4668
  delete data.content;
4615
4669
  }
4616
4670
  return {
@@ -4619,16 +4673,16 @@ function createPageHandlers({
4619
4673
  state: mappedContext.state,
4620
4674
  page: mappedContext.page,
4621
4675
  path: mappedContext.data.path,
4622
- root: isProduction ? null : mappedContext.elements.root,
4623
- customElements: isProduction ? null : mappedContext.elements.customElements,
4624
- tempElements: isProduction ? null : mappedContext.elements.tempElements,
4625
- skipRenderElements: isProduction ? null : mappedContext.elements.skipRenderElements
4676
+ root: isStatic ? null : mappedContext.elements.root,
4677
+ customElements: isStatic ? null : mappedContext.elements.customElements,
4678
+ tempElements: isStatic ? null : mappedContext.elements.tempElements,
4679
+ skipRenderElements: isStatic ? null : mappedContext.elements.skipRenderElements
4626
4680
  },
4627
4681
  state: mappedContext.state
4628
4682
  };
4629
4683
  };
4630
4684
  const onPageUpdateLocal = async (newValue, oldValue) => {
4631
- if (app.options.mode === "production") {
4685
+ if (app.options.mode === "production" || app.options.mode === "testing") {
4632
4686
  if (!newValue.result) {
4633
4687
  await onFileSetLocal(newValue);
4634
4688
  }
@@ -4869,7 +4923,7 @@ ${css}
4869
4923
  root.children.unshift(styleElement);
4870
4924
  }
4871
4925
  }
4872
- function injectReadinessScript(root, head, hasScripts) {
4926
+ function injectReadinessScript(root, head, hasScripts, mode) {
4873
4927
  const readinessScriptElement = createCoraliteElement({
4874
4928
  type: "tag",
4875
4929
  name: "script",
@@ -4878,6 +4932,9 @@ function injectReadinessScript(root, head, hasScripts) {
4878
4932
  children: []
4879
4933
  });
4880
4934
  let data = "class CoraliteLifecycleManager { constructor() { this.defined = new Promise(r => this._dr = r); this.rendered = new Promise(r => this._rr = r); this.hydrated = new Promise(r => this._hr = r); this._t = 0; this._rc = 0; this._hc = 0; this._ts = 0; this._dt = new Set(); this._ip = new WeakMap(); this._ir = new WeakMap(); this._rs = new WeakSet(); this._hs = new WeakSet(); this._s = false; } _start(t, ts) { this._t = t; this._ts = ts; this._s = true; this._check(); } _check() { if (!this._s) return; if (this._rc >= this._t) this._rr(); if (this._hc >= this._t) this._hr(); if (this._dt.size >= this._ts) this._dr(); } _markDefined(tag) { this._dt.add(tag); this._check(); } _markInstanceRendered(el) { if (el.hasAttribute('data-coralite-initial') && !this._rs.has(el)) { this._rs.add(el); this._rc++; this._check(); } } _markInstanceReady(el) { const r = this._ir.get(el); r && r(); this._ip.set(el, Promise.resolve()); if (el.hasAttribute('data-coralite-initial') && !this._hs.has(el)) { this._hs.add(el); this._hc++; this._check(); } } waitFor(el) { let p = this._ip.get(el); if (!p) { p = new Promise(r => this._ir.set(el, r)); this._ip.set(el, p); } return p; } } window.__coralite__ = { lifecycle: new CoraliteLifecycleManager() };";
4935
+ if (mode === "testing") {
4936
+ data += " window.__coralite__.components = {}; window.__coralite__.events = [];";
4937
+ }
4881
4938
  if (!hasScripts) {
4882
4939
  data += " window.__coralite__.lifecycle._start(0, 0);";
4883
4940
  }
@@ -4984,8 +5041,32 @@ import { getClientContext, createCoraliteClass, globalClientHooks } from '${base
4984
5041
  (async () => {
4985
5042
  const hydrationData = ${hydrationData};
4986
5043
  const declarativeTags = ${JSON.stringify(declarativeTags)};
4987
- const initialElements = Array.from(document.querySelectorAll('[data-coralite-initial]'))
4988
- .filter(el => declarativeTags.includes(el.tagName.toLowerCase()));
5044
+
5045
+ const initialElements = Array.from(document.querySelectorAll('[data-cid]'))
5046
+ .filter(el => {
5047
+ const tagName = el.tagName.toLowerCase();
5048
+ const isDeclarative = declarativeTags.includes(tagName);
5049
+ const isInitial = el.hasAttribute('data-coralite-initial');
5050
+
5051
+ if (window['__coralite__'] && window['__coralite__'].components && isInitial) {
5052
+ const cid = el.getAttribute('data-cid');
5053
+ if (cid && !hydrationData[cid]) {
5054
+ const error = new Error('Coralite Hydration Mismatch: Component with data-cid "' + cid + '" (' + tagName + ') has no matching server hydration data.');
5055
+ if (typeof window['showCoraliteError'] === 'function') {
5056
+ window['showCoraliteError'](error);
5057
+ } else {
5058
+ console.error(error);
5059
+ const overlay = document.createElement('div');
5060
+ overlay.style = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(255,0,0,0.9);color:white;padding:20px;z-index:10000;font-family:monospace;white-space:pre-wrap;overflow:auto;';
5061
+ overlay.innerHTML = '<h1>Coralite Hydration Mismatch</h1><p>' + error.message + '</p>';
5062
+ document.body.appendChild(overlay);
5063
+ }
5064
+ throw error;
5065
+ }
5066
+ }
5067
+
5068
+ return isDeclarative && isInitial;
5069
+ });
4989
5070
  if (window.__coralite__ && window.__coralite__.lifecycle) {
4990
5071
  window.__coralite__.lifecycle._start(initialElements.length, declarativeTags.length);
4991
5072
  }
@@ -5424,20 +5505,11 @@ function createRenderer({
5424
5505
  componentState = cleanKeys(componentState);
5425
5506
  }
5426
5507
  const module = cloneModuleInstance(moduleComponent.result);
5427
- if (module.values && module.values.refs) {
5428
- for (let i = 0; i < module.values.refs.length; i++) {
5429
- const ref = module.values.refs[i];
5430
- const uniqueRefValue = `${instanceId}__${ref.name}`;
5431
- if (ref.element && ref.element.attribs) {
5432
- ref.element.attribs.ref = uniqueRefValue;
5433
- }
5434
- componentState[`ref_${ref.name}`] = uniqueRefValue;
5435
- }
5436
- }
5437
5508
  const mappedComponentContext = await hooks.trigger("onBeforeComponentRender", {
5438
5509
  state: componentState,
5439
5510
  componentId: module.id,
5440
5511
  instanceId,
5512
+ template: module.template,
5441
5513
  refs: module.values.refs,
5442
5514
  textNodes: module.values.textNodes,
5443
5515
  attributes: module.values.attributes,
@@ -5447,6 +5519,16 @@ function createRenderer({
5447
5519
  app
5448
5520
  });
5449
5521
  componentState = mappedComponentContext.state;
5522
+ if (module.values && module.values.refs) {
5523
+ for (let i = 0; i < module.values.refs.length; i++) {
5524
+ const ref = module.values.refs[i];
5525
+ const uniqueRefValue = `${instanceId}__${ref.name}`;
5526
+ if (ref.element && ref.element.attribs) {
5527
+ ref.element.attribs.ref = uniqueRefValue;
5528
+ }
5529
+ componentState[`ref_${ref.name}`] = uniqueRefValue;
5530
+ }
5531
+ }
5450
5532
  const result = module.template;
5451
5533
  if (module.styles.length) {
5452
5534
  const selector = module.id;
@@ -6024,7 +6106,7 @@ function createRenderer({
6024
6106
  chunkManifest[tag] = scriptResult.manifest[tag];
6025
6107
  }
6026
6108
  }
6027
- injectReadinessScript(mappedComponent.root, headElement, true);
6109
+ injectReadinessScript(mappedComponent.root, headElement, true, normalizedOptions.mode);
6028
6110
  injectImportMap(mappedComponent.root, headElement, scriptResult.importMap);
6029
6111
  const base = normalizedOptions.baseURL.endsWith("/") ? normalizedOptions.baseURL : normalizedOptions.baseURL + "/";
6030
6112
  const hydrationData = {};
@@ -6057,7 +6139,7 @@ function createRenderer({
6057
6139
  }
6058
6140
  removeElements(mappedComponent.skipRenderElements, true);
6059
6141
  if (!mappedSessionObject.scripts.content[mappedComponent.path.pathname]) {
6060
- injectReadinessScript(mappedComponent.root, headElement, false);
6142
+ injectReadinessScript(mappedComponent.root, headElement, false, normalizedOptions.mode);
6061
6143
  }
6062
6144
  const rawHTML = transformNode(mappedComponent.root);
6063
6145
  const result = {
@@ -6158,7 +6240,7 @@ function createRenderer({
6158
6240
  if (!buildOptions) {
6159
6241
  buildOptions = {};
6160
6242
  }
6161
- if (normalizedOptions.mode === "production") {
6243
+ if (normalizedOptions.mode === "production" || normalizedOptions.mode === "testing") {
6162
6244
  const allComponentIds = app.components.list.map((c) => c.result.id);
6163
6245
  globalScriptResult = await scriptManager.compileAllInstances(allComponentIds, normalizedOptions.mode);
6164
6246
  Object.assign(outputFiles, globalScriptResult.outputFiles);
@@ -6249,12 +6331,16 @@ function createRenderer({
6249
6331
  });
6250
6332
  }
6251
6333
  }
6334
+ const mocksStr = app.options.testing?.mocks ? serialize2(app.options.testing.mocks) : "";
6335
+ const mocksHash = hash(mocksStr);
6336
+ const mocksChanged = manifest.testingMocksHash !== mocksHash;
6252
6337
  const pagesToRender = [];
6253
6338
  const skippedPages = [];
6254
6339
  const newManifest = {
6255
6340
  physical: {},
6256
6341
  virtual: {},
6257
- dependencies: {}
6342
+ dependencies: {},
6343
+ testingMocksHash: mocksHash
6258
6344
  };
6259
6345
  const componentChanges = /* @__PURE__ */ new Map();
6260
6346
  const allComponents = app.components.list;
@@ -6277,7 +6363,7 @@ function createRenderer({
6277
6363
  };
6278
6364
  for (const pageItem of queue) {
6279
6365
  let shouldRebuild = false;
6280
- if (normalizedOptions.output && normalizedOptions.mode === "production") {
6366
+ if (normalizedOptions.output && (normalizedOptions.mode === "production" || normalizedOptions.mode === "testing")) {
6281
6367
  const relativeDir = relative3(normalizedOptions.path.pages, pageItem.path.dirname);
6282
6368
  const outFile = join4(normalizedOptions.output, relativeDir, pageItem.path.filename);
6283
6369
  try {
@@ -6296,6 +6382,9 @@ function createRenderer({
6296
6382
  if (componentIds.some((id) => componentChanges.get(id))) {
6297
6383
  shouldRebuild = true;
6298
6384
  }
6385
+ if (mocksChanged) {
6386
+ shouldRebuild = true;
6387
+ }
6299
6388
  if (pageItem.virtual) {
6300
6389
  if (shouldRebuild || pageItem.volatile || !manifest.virtual || !manifest.virtual[pageItem.path.pathname] || String(manifest.virtual[pageItem.path.pathname].cacheKey) !== String(pageItem.cacheKey) || normalizedOptions.mode === "development") {
6301
6390
  shouldRebuild = true;
@@ -6494,7 +6583,8 @@ async function createCoralite({
6494
6583
  skipRenderByAttribute,
6495
6584
  onError,
6496
6585
  mode = "production",
6497
- output
6586
+ output,
6587
+ testing
6498
6588
  }) {
6499
6589
  if (!components || typeof components !== "string") {
6500
6590
  handleError({
@@ -6530,7 +6620,8 @@ async function createCoralite({
6530
6620
  skipRenderByAttribute,
6531
6621
  mode,
6532
6622
  path: path2,
6533
- output: output ? normalize(output) : void 0
6623
+ output: output ? normalize(output) : void 0,
6624
+ testing
6534
6625
  };
6535
6626
  const trackedOutputFiles = /* @__PURE__ */ new Set();
6536
6627
  const app = {
@@ -6781,7 +6872,7 @@ async function createCoralite({
6781
6872
  return results;
6782
6873
  }
6783
6874
  });
6784
- if (app.options.mode === "development") {
6875
+ if (app.options.mode === "testing") {
6785
6876
  app.options.plugins.unshift(testingPlugin);
6786
6877
  }
6787
6878
  app.options.plugins.unshift(metadataPlugin);
@@ -6839,7 +6930,7 @@ async function createCoralite({
6839
6930
  onUpdate: handlers.onPageUpdate,
6840
6931
  onDelete: handlers.onPageDelete
6841
6932
  });
6842
- if (app.options.mode === "production") {
6933
+ if (app.options.mode === "production" || app.options.mode === "testing") {
6843
6934
  for await (const file of discoverHtmlFiles({
6844
6935
  path: app.options.pages,
6845
6936
  recursive: true,
@@ -6880,6 +6971,30 @@ function defineConfig(options) {
6880
6971
  throw new CoraliteError(`Config property "${prop}" cannot be empty`);
6881
6972
  }
6882
6973
  }
6974
+ if (options.mode !== void 0) {
6975
+ const validModes = ["production", "development", "testing"];
6976
+ if (!validModes.includes(options.mode)) {
6977
+ throw new CoraliteError(`Invalid mode: "${options.mode}". Valid modes are: ${validModes.join(", ")}`);
6978
+ }
6979
+ }
6980
+ if (options.testing !== void 0) {
6981
+ if (typeof options.testing !== "object" || options.testing === null) {
6982
+ throw new CoraliteError('Config property "testing" must be an object');
6983
+ }
6984
+ if (options.testing.mocks !== void 0) {
6985
+ if (typeof options.testing.mocks !== "object" || options.testing.mocks === null) {
6986
+ throw new CoraliteError('Config property "testing.mocks" must be an object');
6987
+ }
6988
+ for (const [key, mock] of Object.entries(options.testing.mocks)) {
6989
+ if (typeof mock !== "object" || mock === null) {
6990
+ throw new CoraliteError(`Mock for component "${key}" must be an object`);
6991
+ }
6992
+ if (mock.server !== void 0 && typeof mock.server !== "function") {
6993
+ throw new CoraliteError(`Mock server for component "${key}" must be a function`);
6994
+ }
6995
+ }
6996
+ }
6997
+ }
6883
6998
  if ("plugins" in options && options.plugins !== void 0) {
6884
6999
  if (!Array.isArray(options.plugins)) {
6885
7000
  throw new CoraliteError(