@rstest/browser 0.10.5 → 0.10.6

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.
@@ -25,6 +25,7 @@ export declare const createBrowserRsbuildDevConfig: (_isWatchMode: boolean) => {
25
25
  logLevel: 'error';
26
26
  };
27
27
  };
28
+ export declare const createBrowserContextExcludeRegExp: (patterns: string[], projectRoot: string) => RegExp | null;
28
29
  // ============================================================================
29
30
  // Main Entry Point
30
31
  // ============================================================================
package/dist/index.js CHANGED
@@ -3002,7 +3002,8 @@ const globToRegexp = (glob)=>{
3002
3002
  const regex = picomatch_default().makeRe(glob, {
3003
3003
  fastpaths: false,
3004
3004
  noglobstar: false,
3005
- bash: false
3005
+ bash: false,
3006
+ dot: true
3006
3007
  });
3007
3008
  if (!regex) throw new Error(`Invalid glob pattern: ${glob}`);
3008
3009
  if (!glob.startsWith('./')) return regex;
@@ -3022,20 +3023,76 @@ const globPatternsToRegExp = (patterns)=>{
3022
3023
  });
3023
3024
  return new RegExp(`(?:${regexParts.join('|')})$`);
3024
3025
  };
3025
- const excludePatternsToRegExp = (patterns)=>{
3026
- const keywords = [];
3027
- for (const pattern of patterns){
3028
- const match = pattern.match(/\*\*\/\.?\{?([^/*{}]+(?:,[^/*{}]+)*)\}?\/?\*?\*?/);
3029
- if (match) {
3030
- const parts = match[1].split(',');
3031
- for (const part of parts){
3032
- const cleaned = part.replace(/^\./, '');
3033
- if (cleaned && !keywords.includes(cleaned)) keywords.push(cleaned);
3034
- }
3026
+ const REGEXP_SPECIAL_CHARACTERS = /[|\\{}()[\]^$+*?.]/g;
3027
+ const PATH_SEPARATOR_SOURCE = String.raw`[\\/]`;
3028
+ const WINDOWS_ABSOLUTE_PATH_SOURCE = String.raw`[A-Za-z]:[\\/]`;
3029
+ const escapeRegExp = (value)=>value.replace(REGEXP_SPECIAL_CHARACTERS, '\\$&');
3030
+ const normalizePathForRegExp = (value)=>normalize(value).replaceAll('\\', '/');
3031
+ const normalizeExcludePatternForRegExp = (value)=>value.startsWith('./') ? `./${normalizePathForRegExp(value.substring(2))}` : normalizePathForRegExp(value);
3032
+ const isAbsolutePatternForRegExp = (value)=>value.startsWith('/') || /^[A-Za-z]:\//.test(value);
3033
+ const isEscapedRegExpCharacter = (source, index)=>{
3034
+ let backslashCount = 0;
3035
+ for(let current = index - 1; current >= 0 && '\\' === source[current]; current--)backslashCount++;
3036
+ return backslashCount % 2 === 1;
3037
+ };
3038
+ const replacePathSeparatorsInRegExpSource = (source)=>{
3039
+ let result = '';
3040
+ let inCharacterClass = false;
3041
+ for(let index = 0; index < source.length; index++){
3042
+ const character = source[index];
3043
+ const isEscaped = isEscapedRegExpCharacter(source, index);
3044
+ if ('[' === character && !isEscaped) inCharacterClass = true;
3045
+ if (!inCharacterClass && '\\' === character && '/' === source[index + 1]) {
3046
+ result += PATH_SEPARATOR_SOURCE;
3047
+ index++;
3048
+ continue;
3035
3049
  }
3050
+ result += character;
3051
+ if (']' === character && !isEscaped) inCharacterClass = false;
3052
+ }
3053
+ return result;
3054
+ };
3055
+ const createRelativeContextExcludeSource = (source, normalizedPattern)=>{
3056
+ if (normalizedPattern.startsWith('./')) return source;
3057
+ return normalizedPattern.startsWith('**/') ? `(?:(?:${source})|\\.(?:${source}))` : `(?:(?:${source})|\\.${PATH_SEPARATOR_SOURCE}(?:${source}))`;
3058
+ };
3059
+ const createProjectAbsoluteExcludeSource = (source, normalizedPattern)=>normalizedPattern.startsWith('./') || normalizedPattern.startsWith('**/') ? source : `${PATH_SEPARATOR_SOURCE}(?:${source})`;
3060
+ const excludePatternsToRegExpSources = (patterns)=>{
3061
+ const sources = patterns.map((pattern)=>{
3062
+ const normalizedPattern = normalizeExcludePatternForRegExp(pattern);
3063
+ const regex = globToRegexp(normalizedPattern);
3064
+ let source = regex.source;
3065
+ if (source.startsWith('^')) source = source.substring(1);
3066
+ if (source.endsWith('$')) source = source.substring(0, source.length - 1);
3067
+ source = replacePathSeparatorsInRegExpSource(source);
3068
+ const isAbsolute = isAbsolutePatternForRegExp(normalizedPattern);
3069
+ const absolute = normalizedPattern.startsWith('./') ? source.substring(2) : source;
3070
+ return {
3071
+ relative: isAbsolute ? source : createRelativeContextExcludeSource(source, normalizedPattern),
3072
+ absolute: isAbsolute ? absolute : createProjectAbsoluteExcludeSource(absolute, normalizedPattern),
3073
+ isAbsolute
3074
+ };
3075
+ });
3076
+ if (0 === sources.length) return null;
3077
+ return sources;
3078
+ };
3079
+ const createBrowserContextExcludeRegExp = (patterns, projectRoot)=>{
3080
+ const excludeSources = excludePatternsToRegExpSources(patterns);
3081
+ if (!excludeSources) return null;
3082
+ const normalizedProjectRoot = normalizePathForRegExp(projectRoot).replace(/[\\/]$/, '');
3083
+ const projectRootSource = normalizedProjectRoot.split('/').map(escapeRegExp).join(PATH_SEPARATOR_SOURCE);
3084
+ const relativeExcludeSources = excludeSources.filter((source)=>!source.isAbsolute);
3085
+ const absoluteExcludeSources = excludeSources.filter((source)=>source.isAbsolute);
3086
+ const sourceBranches = [];
3087
+ if (relativeExcludeSources.length > 0) {
3088
+ const relativePatternSource = `(?:${relativeExcludeSources.map((source)=>source.relative).join('|')})`;
3089
+ const absolutePatternSource = `(?:${relativeExcludeSources.map((source)=>source.absolute).join('|')})`;
3090
+ const relativeSource = `(?:${relativePatternSource})`;
3091
+ const absoluteSource = normalizedProjectRoot ? `${projectRootSource}(?=${PATH_SEPARATOR_SOURCE})(?:${absolutePatternSource})` : `(?:${absolutePatternSource})`;
3092
+ sourceBranches.push(`(?!${WINDOWS_ABSOLUTE_PATH_SOURCE}|${PATH_SEPARATOR_SOURCE})${relativeSource}`, absoluteSource);
3036
3093
  }
3037
- if (0 === keywords.length) return null;
3038
- return new RegExp(`[\\\\/](${keywords.join('|')})[\\\\/]`);
3094
+ if (absoluteExcludeSources.length > 0) sourceBranches.push(`(?:${absoluteExcludeSources.map((source)=>source.relative).join('|')})`);
3095
+ return new RegExp(`^(?:${sourceBranches.join('|')})$`);
3039
3096
  };
3040
3097
  const findTestFileInModules = (modules, entryTestFiles)=>{
3041
3098
  if (!modules) return null;
@@ -3211,7 +3268,7 @@ const generateManifestModule = ({ manifestPath, entries })=>{
3211
3268
  const projectRootPosix = normalize(project.rootPath);
3212
3269
  const includeRegExp = globPatternsToRegExp(project.normalizedConfig.include);
3213
3270
  const excludePatterns = project.normalizedConfig.exclude.patterns;
3214
- const excludeRegExp = excludePatternsToRegExp(excludePatterns);
3271
+ const excludeRegExp = createBrowserContextExcludeRegExp(excludePatterns, projectRootPosix);
3215
3272
  lines.push(`const ${varName} = import.meta.webpackContext(${JSON.stringify(projectRootPosix)}, {`);
3216
3273
  lines.push(' recursive: true,');
3217
3274
  lines.push(` regExp: ${includeRegExp.toString()},`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rstest/browser",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "description": "Browser mode support for Rstest testing framework.",
5
5
  "keywords": [
6
6
  "rstest",
@@ -48,21 +48,21 @@
48
48
  "ws": "^8.21.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@rslib/core": "0.22.1",
51
+ "@rslib/core": "0.23.0",
52
52
  "@types/convert-source-map": "^2.0.3",
53
53
  "@types/picomatch": "^4.0.3",
54
54
  "@types/ws": "^8.18.1",
55
55
  "@vitest/snapshot": "^3.2.6",
56
56
  "birpc": "^4.0.0",
57
57
  "picomatch": "^4.0.4",
58
- "playwright": "^1.60.0",
58
+ "playwright": "^1.61.0",
59
59
  "@rstest/browser-ui": "0.0.0",
60
- "@rstest/core": "0.10.5",
61
- "@rstest/tsconfig": "0.0.1"
60
+ "@rstest/tsconfig": "0.0.1",
61
+ "@rstest/core": "0.10.6"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "playwright": "^1.49.1",
65
- "@rstest/core": "^0.10.5"
65
+ "@rstest/core": "^0.10.6"
66
66
  },
67
67
  "peerDependenciesMeta": {
68
68
  "playwright": {
@@ -629,6 +629,7 @@ const globToRegexp = (glob: string): RegExp => {
629
629
  fastpaths: false,
630
630
  noglobstar: false,
631
631
  bash: false,
632
+ dot: true,
632
633
  });
633
634
 
634
635
  if (!regex) {
@@ -676,44 +677,189 @@ const globPatternsToRegExp = (patterns: string[]): RegExp => {
676
677
  return new RegExp(`(?:${regexParts.join('|')})$`);
677
678
  };
678
679
 
680
+ const REGEXP_SPECIAL_CHARACTERS = /[|\\{}()[\]^$+*?.]/g;
681
+ const PATH_SEPARATOR_SOURCE = String.raw`[\\/]`;
682
+ const WINDOWS_ABSOLUTE_PATH_SOURCE = String.raw`[A-Za-z]:[\\/]`;
683
+
684
+ const escapeRegExp = (value: string): string =>
685
+ value.replace(REGEXP_SPECIAL_CHARACTERS, '\\$&');
686
+
687
+ const normalizePathForRegExp = (value: string): string =>
688
+ normalize(value).replaceAll('\\', '/');
689
+
690
+ const normalizeExcludePatternForRegExp = (value: string): string =>
691
+ value.startsWith('./')
692
+ ? `./${normalizePathForRegExp(value.substring(2))}`
693
+ : normalizePathForRegExp(value);
694
+
695
+ const isAbsolutePatternForRegExp = (value: string): boolean =>
696
+ value.startsWith('/') || /^[A-Za-z]:\//.test(value);
697
+
698
+ const isEscapedRegExpCharacter = (source: string, index: number): boolean => {
699
+ let backslashCount = 0;
700
+ for (
701
+ let current = index - 1;
702
+ current >= 0 && source[current] === '\\';
703
+ current--
704
+ ) {
705
+ backslashCount++;
706
+ }
707
+ return backslashCount % 2 === 1;
708
+ };
709
+
710
+ const replacePathSeparatorsInRegExpSource = (source: string): string => {
711
+ let result = '';
712
+ let inCharacterClass = false;
713
+
714
+ for (let index = 0; index < source.length; index++) {
715
+ const character = source[index];
716
+ const isEscaped = isEscapedRegExpCharacter(source, index);
717
+
718
+ if (character === '[' && !isEscaped) {
719
+ inCharacterClass = true;
720
+ }
721
+
722
+ if (!inCharacterClass && character === '\\' && source[index + 1] === '/') {
723
+ result += PATH_SEPARATOR_SOURCE;
724
+ index++;
725
+ continue;
726
+ }
727
+
728
+ result += character;
729
+
730
+ if (character === ']' && !isEscaped) {
731
+ inCharacterClass = false;
732
+ }
733
+ }
734
+
735
+ return result;
736
+ };
737
+
738
+ type BrowserContextExcludeSource = {
739
+ relative: string;
740
+ absolute: string;
741
+ isAbsolute: boolean;
742
+ };
743
+
744
+ const createRelativeContextExcludeSource = (
745
+ source: string,
746
+ normalizedPattern: string,
747
+ ): string => {
748
+ if (normalizedPattern.startsWith('./')) {
749
+ return source;
750
+ }
751
+
752
+ return normalizedPattern.startsWith('**/')
753
+ ? `(?:(?:${source})|\\.(?:${source}))`
754
+ : `(?:(?:${source})|\\.${PATH_SEPARATOR_SOURCE}(?:${source}))`;
755
+ };
756
+
757
+ const createProjectAbsoluteExcludeSource = (
758
+ source: string,
759
+ normalizedPattern: string,
760
+ ): string =>
761
+ normalizedPattern.startsWith('./') || normalizedPattern.startsWith('**/')
762
+ ? source
763
+ : `${PATH_SEPARATOR_SOURCE}(?:${source})`;
764
+
679
765
  /**
680
766
  * Convert exclude patterns to a RegExp for import.meta.webpackContext's exclude option
681
767
  * This is used at compile time to filter out files during bundling
682
768
  *
683
769
  * Example:
684
770
  * Input: ['**\/node_modules\/**', '**\/dist\/**']
685
- * Output: /[\\/](node_modules|dist)[\\/]/
771
+ * Output: a regexp matching node_modules or dist path segments.
686
772
  */
687
- const excludePatternsToRegExp = (patterns: string[]): RegExp | null => {
688
- const keywords: string[] = [];
689
- for (const pattern of patterns) {
690
- // Extract the core part between ** wildcards
691
- // e.g., '**/node_modules/**' -> 'node_modules'
692
- // e.g., '**/dist/**' -> 'dist'
693
- // e.g., '**/.{idea,git,cache,output,temp}/**' -> extract each part
694
- const match = pattern.match(
695
- /\*\*\/\.?\{?([^/*{}]+(?:,[^/*{}]+)*)\}?\/?\*?\*?/,
696
- );
697
- if (match) {
698
- // Handle {a,b,c} patterns
699
- const parts = match[1]!.split(',');
700
- for (const part of parts) {
701
- // Clean up the part (remove leading dots for hidden dirs)
702
- const cleaned = part.replace(/^\./, '');
703
- if (cleaned && !keywords.includes(cleaned)) {
704
- keywords.push(cleaned);
705
- }
706
- }
773
+ const excludePatternsToRegExpSources = (
774
+ patterns: string[],
775
+ ): BrowserContextExcludeSource[] | null => {
776
+ const sources = patterns.map((pattern) => {
777
+ const normalizedPattern = normalizeExcludePatternForRegExp(pattern);
778
+ const regex = globToRegexp(normalizedPattern);
779
+ let source = regex.source;
780
+ if (source.startsWith('^')) {
781
+ source = source.substring(1);
707
782
  }
783
+ if (source.endsWith('$')) {
784
+ source = source.substring(0, source.length - 1);
785
+ }
786
+
787
+ source = replacePathSeparatorsInRegExpSource(source);
788
+ const isAbsolute = isAbsolutePatternForRegExp(normalizedPattern);
789
+ const absolute = normalizedPattern.startsWith('./')
790
+ ? source.substring(2)
791
+ : source;
792
+
793
+ return {
794
+ relative: isAbsolute
795
+ ? source
796
+ : createRelativeContextExcludeSource(source, normalizedPattern),
797
+ absolute: isAbsolute
798
+ ? absolute
799
+ : createProjectAbsoluteExcludeSource(absolute, normalizedPattern),
800
+ isAbsolute,
801
+ };
802
+ });
803
+
804
+ if (sources.length === 0) {
805
+ return null;
708
806
  }
709
807
 
710
- if (keywords.length === 0) {
808
+ return sources;
809
+ };
810
+
811
+ export const createBrowserContextExcludeRegExp = (
812
+ patterns: string[],
813
+ projectRoot: string,
814
+ ): RegExp | null => {
815
+ const excludeSources = excludePatternsToRegExpSources(patterns);
816
+ if (!excludeSources) {
711
817
  return null;
712
818
  }
713
819
 
714
- // Create regex that matches paths containing these directory names
715
- // Use [\\/] to match both forward and back slashes
716
- return new RegExp(`[\\\\/](${keywords.join('|')})[\\\\/]`);
820
+ const normalizedProjectRoot = normalizePathForRegExp(projectRoot).replace(
821
+ /[\\/]$/,
822
+ '',
823
+ );
824
+ const projectRootSource = normalizedProjectRoot
825
+ .split('/')
826
+ .map(escapeRegExp)
827
+ .join(PATH_SEPARATOR_SOURCE);
828
+ const relativeExcludeSources = excludeSources.filter(
829
+ (source) => !source.isAbsolute,
830
+ );
831
+ const absoluteExcludeSources = excludeSources.filter(
832
+ (source) => source.isAbsolute,
833
+ );
834
+ const sourceBranches: string[] = [];
835
+
836
+ if (relativeExcludeSources.length > 0) {
837
+ const relativePatternSource = `(?:${relativeExcludeSources
838
+ .map((source) => source.relative)
839
+ .join('|')})`;
840
+ const absolutePatternSource = `(?:${relativeExcludeSources
841
+ .map((source) => source.absolute)
842
+ .join('|')})`;
843
+ const relativeSource = `(?:${relativePatternSource})`;
844
+ const absoluteSource = normalizedProjectRoot
845
+ ? `${projectRootSource}(?=${PATH_SEPARATOR_SOURCE})(?:${absolutePatternSource})`
846
+ : `(?:${absolutePatternSource})`;
847
+
848
+ sourceBranches.push(
849
+ `(?!${WINDOWS_ABSOLUTE_PATH_SOURCE}|${PATH_SEPARATOR_SOURCE})${relativeSource}`,
850
+ absoluteSource,
851
+ );
852
+ }
853
+
854
+ if (absoluteExcludeSources.length > 0) {
855
+ sourceBranches.push(
856
+ `(?:${absoluteExcludeSources
857
+ .map((source) => source.relative)
858
+ .join('|')})`,
859
+ );
860
+ }
861
+
862
+ return new RegExp(`^(?:${sourceBranches.join('|')})$`);
717
863
  };
718
864
 
719
865
  type StatsModule = {
@@ -1087,7 +1233,10 @@ const generateManifestModule = ({
1087
1233
  project.normalizedConfig.include,
1088
1234
  );
1089
1235
  const excludePatterns = project.normalizedConfig.exclude.patterns;
1090
- const excludeRegExp = excludePatternsToRegExp(excludePatterns);
1236
+ const excludeRegExp = createBrowserContextExcludeRegExp(
1237
+ excludePatterns,
1238
+ projectRootPosix,
1239
+ );
1091
1240
 
1092
1241
  lines.push(
1093
1242
  `const ${varName} = import.meta.webpackContext(${JSON.stringify(projectRootPosix)}, {`,