tosijs-ui 1.5.7 → 1.5.8

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.
@@ -288,8 +288,8 @@ export function createDocBrowser(options) {
288
288
  }
289
289
  testResultsResolve(allResults);
290
290
  testResultsResolve = undefined;
291
- // Post results to dev server on localhost
292
- if (isLocalhost) {
291
+ // Post results to dev server on localhost (not from test iframes)
292
+ if (isLocalhost && !isTestFrame) {
293
293
  fetch('/report', {
294
294
  method: 'POST',
295
295
  headers: { 'Content-Type': 'application/json' },
@@ -680,17 +680,46 @@ export function createDocBrowser(options) {
680
680
  lines.unshift(`**Summary: ${totalPassed} passed, ${totalFailed} failed**`, '');
681
681
  return lines.join('\n');
682
682
  }
683
+ // Detect if running as background test iframe
684
+ const searchParams = new URLSearchParams(window.location.search);
685
+ const isTestFrame = searchParams.get('_testMode') === '1';
686
+ const testFrameFilename = isTestFrame
687
+ ? window.location.search.substring(1).split('&')[0]
688
+ : null;
683
689
  // Listen for test completion events
684
690
  container.addEventListener('testcomplete', ((event) => {
685
691
  handleTestComplete(event);
686
692
  updateTestWidget();
693
+ // If running in test iframe, post results to parent
694
+ if (isTestFrame && window.parent !== window && testFrameFilename) {
695
+ const { results } = event.detail;
696
+ window.parent.postMessage({ type: 'tosi-test-results', filename: testFrameFilename, results }, '*');
697
+ }
687
698
  }));
699
+ // If running as test iframe, signal when all tests on this page are done
700
+ if (isTestFrame && testFrameFilename) {
701
+ const signalDone = () => {
702
+ const examples = container.querySelectorAll('tosi-example');
703
+ const withTests = [...examples].filter((ex) => ex.classList.contains('-has-tests'));
704
+ const running = withTests.filter((ex) => ex.classList.contains('-test-running'));
705
+ if (withTests.length > 0 && running.length === 0) {
706
+ window.parent.postMessage({ type: 'tosi-tests-done', filename: testFrameFilename }, '*');
707
+ }
708
+ else {
709
+ setTimeout(signalDone, 100);
710
+ }
711
+ };
712
+ // Give time for examples to start running
713
+ setTimeout(signalDone, 500);
714
+ }
688
715
  // Background test runner for all doc pages
689
716
  const runBackgroundTests = async () => {
690
717
  if (backgroundTestsStarted)
691
718
  return;
692
719
  if (!testManager.enabled.value)
693
720
  return;
721
+ if (isTestFrame)
722
+ return; // Don't run background tests in test iframe
694
723
  backgroundTestsStarted = true;
695
724
  // Find all docs that have test blocks
696
725
  const docsWithTests = docs.filter((doc) => doc.text.includes('```test'));
@@ -699,89 +728,71 @@ export function createDocBrowser(options) {
699
728
  setTestWidgetRunning();
700
729
  }
701
730
  if (pagesWithTests === 0) {
702
- // No tests to run, resolve immediately
703
731
  if (testResultsResolve) {
704
732
  testResultsResolve({ passed: 0, failed: 0, pages: {} });
705
733
  testResultsResolve = undefined;
706
734
  }
707
735
  return;
708
736
  }
709
- // Create a hidden iframe to run tests in background
737
+ const currentFilename = String(app.currentDoc.filename);
738
+ // Create a hidden iframe that loads the full page
710
739
  const testFrame = document.createElement('iframe');
711
740
  testFrame.style.cssText =
712
741
  'position: fixed; left: 0; top: 0; width: 800px; height: 600px; opacity: 0; pointer-events: none;';
713
742
  document.body.appendChild(testFrame);
714
- const currentFilename = String(app.currentDoc.filename);
743
+ // Listen for test results posted from the iframe
744
+ const messageHandler = (event) => {
745
+ if (event.data?.type !== 'tosi-test-results')
746
+ return;
747
+ const { filename, results } = event.data;
748
+ if (!pageTestResults[filename]) {
749
+ pageTestResults[filename] = {
750
+ passed: true,
751
+ tests: [],
752
+ totalPassed: 0,
753
+ totalFailed: 0,
754
+ };
755
+ }
756
+ const pageResults = pageTestResults[filename];
757
+ pageResults.tests.push(...results.tests);
758
+ pageResults.totalPassed += results.passed;
759
+ pageResults.totalFailed += results.failed;
760
+ pageResults.passed = pageResults.totalFailed === 0;
761
+ updateDocTestStatus(filename);
762
+ updateTestWidget();
763
+ };
764
+ window.addEventListener('message', messageHandler);
715
765
  for (const doc of docsWithTests) {
716
- // Skip current page - it will run tests naturally
717
- if (doc.filename === currentFilename) {
766
+ // Skip current page it runs tests naturally
767
+ if (doc.filename === currentFilename)
718
768
  continue;
719
- }
720
- // Reset page results for this doc
721
- pageTestResults[doc.filename] = {
722
- passed: true,
723
- tests: [],
724
- totalPassed: 0,
725
- totalFailed: 0,
726
- };
727
- // Create a container and render the doc content
728
- const testContainer = document.createElement('div');
729
- const viewer = tosiMd({
730
- value: doc.text,
731
- didRender() {
732
- LiveExample.insertExamples(this, context);
733
- },
769
+ // Navigate iframe to the page
770
+ const base = window.location.origin + window.location.pathname;
771
+ testFrame.src = `${base}?${doc.filename}&_testMode=1`;
772
+ // Wait for the iframe to signal it's done (max 30s per page)
773
+ await new Promise((resolve) => {
774
+ const deadline = Date.now() + 30_000;
775
+ const onDone = (event) => {
776
+ if (event.data?.type === 'tosi-tests-done' &&
777
+ event.data.filename === doc.filename) {
778
+ window.removeEventListener('message', onDone);
779
+ resolve();
780
+ }
781
+ };
782
+ window.addEventListener('message', onDone);
783
+ setTimeout(() => {
784
+ window.removeEventListener('message', onDone);
785
+ resolve();
786
+ }, deadline - Date.now());
734
787
  });
735
- testContainer.appendChild(viewer);
736
- // Listen for test results from this container
737
- const handleBgTest = (event) => {
738
- const { results } = event.detail;
739
- const pageResults = pageTestResults[doc.filename];
740
- pageResults.tests.push(...results.tests);
741
- pageResults.totalPassed += results.passed;
742
- pageResults.totalFailed += results.failed;
743
- pageResults.passed = pageResults.totalFailed === 0;
744
- updateDocTestStatus(doc.filename);
745
- updateTestWidget();
746
- };
747
- testContainer.addEventListener('testcomplete', handleBgTest);
748
- // Append to iframe for execution
749
- const frameDoc = testFrame.contentDocument;
750
- if (frameDoc) {
751
- frameDoc.body.innerHTML = '';
752
- frameDoc.body.appendChild(testContainer);
753
- // Wait for all live examples with tests to finish (max 30s per page)
754
- await new Promise((resolve) => {
755
- const deadline = Date.now() + 30_000;
756
- const checkDone = () => {
757
- if (Date.now() > deadline) {
758
- resolve();
759
- return;
760
- }
761
- const examples = testContainer.querySelectorAll('tosi-example');
762
- const withTests = [...examples].filter((ex) => ex.classList.contains('-has-tests'));
763
- const running = withTests.filter((ex) => ex.classList.contains('-test-running'));
764
- if (withTests.length > 0 && running.length === 0) {
765
- resolve();
766
- }
767
- else {
768
- setTimeout(checkDone, 100);
769
- }
770
- };
771
- // Give initial render time to start
772
- setTimeout(checkDone, 200);
773
- });
774
- }
775
788
  markPageTested(doc.filename);
776
789
  }
777
790
  // Clean up
791
+ window.removeEventListener('message', messageHandler);
778
792
  testFrame.remove();
779
793
  // Mark current page as tested if it has tests
780
794
  if (docsWithTests.some((d) => d.filename === currentFilename)) {
781
- // Current page tests will complete naturally, just wait a bit
782
- setTimeout(() => {
783
- markPageTested(currentFilename);
784
- }, 1000);
795
+ setTimeout(() => markPageTested(currentFilename), 1000);
785
796
  }
786
797
  };
787
798
  // Run background tests when enabled (initially or when toggled on)
package/dist/icons.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface IconRule {
8
8
  prefix: string | RegExp;
9
9
  apply: (baseName: string, match: RegExpMatchArray | string, parts: ElementPart[]) => Element | string | null;
10
10
  }
11
+ /** Returns icon name safe for suffix concatenation (appends _ if name ends in digit) */
12
+ export declare const safeIconSuffix: (name: string) => string;
11
13
  export declare const iconRules: IconRule[];
12
14
  export declare const icons: SVGIconMap;
13
15
  export declare class SvgIcon extends WebComponent {
package/dist/icons.js CHANGED
@@ -573,6 +573,8 @@ export const svg2DataUrl = (icon, fill, stroke, strokeWidth) => {
573
573
  const text = encodeURIComponent(svg.outerHTML);
574
574
  return `url(data:image/svg+xml;charset=UTF-8,${text})`;
575
575
  };
576
+ /** Returns icon name safe for suffix concatenation (appends _ if name ends in digit) */
577
+ export const safeIconSuffix = (name) => /\d$/.test(name) ? name + '_' : name;
576
578
  export const iconRules = [
577
579
  {
578
580
  prefix: /^spin(_?\d+)/,
@@ -601,19 +603,19 @@ export const iconRules = [
601
603
  },
602
604
  {
603
605
  prefix: 'un',
604
- apply: (baseName) => `slash25o$${baseName}75s75o`,
606
+ apply: (baseName) => `slash25o$${safeIconSuffix(baseName)}75s75o`,
605
607
  },
606
608
  {
607
609
  prefix: 'check',
608
- apply: (baseName) => `check75o_00aa00S$${baseName}75s50o`,
610
+ apply: (baseName) => `check75o_00aa00S$${safeIconSuffix(baseName)}75s50o`,
609
611
  },
610
612
  {
611
613
  prefix: 'cancel',
612
- apply: (baseName) => `x75o_cc0000S$${baseName}75s50o`,
614
+ apply: (baseName) => `x75o_cc0000S$${safeIconSuffix(baseName)}75s50o`,
613
615
  },
614
616
  {
615
617
  prefix: 'search',
616
- apply: (baseName) => `search80s30x30y$${baseName}50o`,
618
+ apply: (baseName) => `search80s30x30y$${safeIconSuffix(baseName)}50o`,
617
619
  },
618
620
  ];
619
621
  function makeIcon(spec, parts) {
@@ -706,21 +708,32 @@ function composeIcon(prop, parts) {
706
708
  return null;
707
709
  }
708
710
  const MAX_REDIRECTS = 10;
709
- // Style suffixes — always value then letter code:
711
+ // Style suffixes — value then letter code:
710
712
  // 50o (opacity), 75s (scale), 20x (translateX%), _10y (translateY%)
711
713
  // 90r (rotate 90°), _45r (rotate -45°), 0f (flipH), 1f (flipV)
712
714
  // _FF0000F (fill hex), _f00S (stroke hex), 3W (stroke-width)
713
715
  // _brandColorF (fill var), _accentS (stroke var)
714
- const SUFFIX_RE = /(_?\d{2,3}[osxyr]|[01]f|_[a-zA-Z0-9]+[FS]|\d{1,3}W)+$/;
716
+ // Icon names ending in digits need _ separator: edit2_50o
717
+ const SUFFIX_RE = /(?:(?<=[a-zA-Z_])(?:_?\d+[osxyr]|[01]f|\d+W)|_[a-zA-Z0-9]+[FS])+$/;
715
718
  function parseStyleSuffixes(name) {
716
719
  const match = name.match(SUFFIX_RE);
717
720
  if (!match)
718
721
  return null;
719
- const baseName = name.slice(0, match.index);
722
+ let baseName = name.slice(0, match.index);
720
723
  if (!baseName)
721
724
  return null;
725
+ // Strip trailing _ separator (for digit-ending names: edit2_50o)
726
+ if (baseName.endsWith('_'))
727
+ baseName = baseName.slice(0, -1);
728
+ if (!baseName)
729
+ return null;
730
+ const data = iconData;
731
+ if (!data[baseName])
732
+ return null;
722
733
  const style = {};
723
- const suffixes = match[0].match(/_?\d{2,3}[osxyr]|[01]f|_[a-zA-Z0-9]+[FS]|\d{1,3}W/g);
734
+ const suffixes = match[0].match(/_?\d+[osxyr]|[01]f|_[a-zA-Z0-9]+[FS]|\d+W/g);
735
+ if (!suffixes)
736
+ return null;
724
737
  let tx = '';
725
738
  let ty = '';
726
739
  let scale = '';