@sungen/driver-ui 3.2.5 → 3.2.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sungen/driver-ui",
3
- "version": "3.2.5",
3
+ "version": "3.2.7",
4
4
  "description": "Sungen UI capability (Playwright) — step patterns, viewpoint gate, and phase hooks. Plugs into @sun-asterisk/sungen via the capability SPI.",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -13,7 +13,7 @@
13
13
  "author": "eqe team (engineer & quality) — Sun Asterisk",
14
14
  "license": "MIT",
15
15
  "dependencies": {
16
- "@sun-asterisk/sungen": "3.2.5"
16
+ "@sun-asterisk/sungen": "3.2.7"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "@playwright/test": "^1.57.0"
@@ -1,5 +1,6 @@
1
1
  import { ParsedStep } from '@sun-asterisk/sungen';
2
2
  import { StepPattern, StepTemplateData } from '@sun-asterisk/sungen';
3
+ import { pathToRegexSource } from '../utils/url-path-regex';
3
4
 
4
5
  /**
5
6
  * Assertion patterns: visibility, text content, state, attributes
@@ -73,7 +74,10 @@ export const assertionPatterns: StepPattern[] = [
73
74
  step.text.includes('sees')) &&
74
75
  step.elementType === 'page',
75
76
  resolver: (step, context): StepTemplateData => {
76
- let path = step.featurePath || '/';
77
+ // Fallback is the feature's declared Path (context.featurePath) the page under test.
78
+ // `step.featurePath` is never populated by the parser, so it always degraded to '/'.
79
+ const fallbackPath = context.featurePath || '/';
80
+ let path = fallbackPath;
77
81
 
78
82
  // If selector is present, extract path from selector's value attribute
79
83
  if (step.selectorRef) {
@@ -84,16 +88,20 @@ export const assertionPatterns: StepPattern[] = [
84
88
  step.elementType,
85
89
  step.nth
86
90
  );
87
- // Use selector's value as the path for URL assertion
88
- path = resolved.value || path;
91
+ // Only a `page`-type entry carries a URL in `value`. Selector keys can collide across
92
+ // types (a single `読取結果一覧` entry of type `role` reused for a page assertion), and a
93
+ // key-only lookup happily returns that button entry — whose `value` ('button') is not a
94
+ // URL. Guard on the entry kind: use `value` only for a page selector, else fall back to
95
+ // the feature path rather than compiling `toHaveURL(/button/)`.
96
+ path = resolved.selectorType === 'page' ? (resolved.value || fallbackPath) : fallbackPath;
89
97
  } catch (error) {
90
98
  // Fallback to feature path if selector not found
91
- path = step.featurePath || '/';
99
+ path = fallbackPath;
92
100
  }
93
101
  }
94
102
 
95
- // Convert path to regex-safe string
96
- const pathRegex = path.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
103
+ // Convert path to regex-safe string (dynamic :id segments → [^/]+ wildcard)
104
+ const pathRegex = pathToRegexSource(path);
97
105
  return {
98
106
  templateName: 'page-assertion',
99
107
  data: { pathRegex },
@@ -1,5 +1,6 @@
1
1
  import { ParsedStep } from '@sun-asterisk/sungen';
2
2
  import { StepPattern } from '@sun-asterisk/sungen';
3
+ import { escapeRegex, pathToRegexSource } from '../utils/url-path-regex';
3
4
 
4
5
  /**
5
6
  * Extract Playwright waitFor state from step text.
@@ -296,14 +297,20 @@ export const interactionPatterns: StepPattern[] = [
296
297
  matcher: (step: ParsedStep) =>
297
298
  (step.text.includes('wait for') || step.text.includes('waits for')) && step.elementType === 'page',
298
299
  resolver: (step, context) => {
299
- let path = step.featurePath || '/';
300
+ // Fallback is the feature's declared Path (context.featurePath) — `step.featurePath` is
301
+ // never populated by the parser, so it always degraded to '/'.
302
+ const fallbackPath = context.featurePath || '/';
303
+ let path = fallbackPath;
300
304
 
301
305
  if (step.selectorRef) {
302
306
  try {
303
307
  const resolved = context.selectorResolver.resolveSelector(
304
308
  step.selectorRef, context.featureName, step.elementType, step.nth
305
309
  );
306
- path = resolved.value || path;
310
+ // Only a `page`-type entry carries a URL in `value`. A key collision with a non-page
311
+ // entry (e.g. a role/button sharing the key) must fall back to featurePath, never feed
312
+ // its value ('button') into waitForURL. See page-assertion for the mirror guard.
313
+ path = resolved.selectorType === 'page' ? (resolved.value || fallbackPath) : fallbackPath;
307
314
  } catch (error) {
308
315
  // fallback to featurePath
309
316
  }
@@ -313,11 +320,12 @@ export const interactionPatterns: StepPattern[] = [
313
320
  let pathRegex: string;
314
321
  if (isAbsoluteUrl) {
315
322
  const url = new URL(path);
316
- const hostEscaped = url.hostname.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
317
- const pathEscaped = url.pathname !== '/' ? url.pathname.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&') : '';
323
+ const hostEscaped = escapeRegex(url.hostname);
324
+ // Dynamic :id segments in the path [^/]+ wildcard (host stays literal)
325
+ const pathEscaped = url.pathname !== '/' ? pathToRegexSource(url.pathname) : '';
318
326
  pathRegex = hostEscaped + pathEscaped;
319
327
  } else {
320
- pathRegex = path.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
328
+ pathRegex = pathToRegexSource(path);
321
329
  }
322
330
 
323
331
  return {
@@ -0,0 +1,31 @@
1
+ /**
2
+ * URL-path → RegExp-source conversion for page URL assertions (`toHaveURL`) and
3
+ * navigation waits (`waitForURL`).
4
+ *
5
+ * A selector's `value` may carry dynamic route placeholders (`:id`, `:receptionId`,
6
+ * …), e.g. `/cn22/view/so/ocr/results/:id`. A live URL populates those segments with
7
+ * real values (`…/results/rec-dummy-000017`), so a fully-escaped literal pattern would
8
+ * never match. Placeholder segments become a single-segment wildcard (`[^/]+`); every
9
+ * other segment is escaped so its regex metacharacters stay literal.
10
+ */
11
+
12
+ const REGEX_SPECIALS = /[.*+?^${}()|[\]\\/]/g;
13
+
14
+ /** Escape a literal string for safe use inside a RegExp source. */
15
+ export function escapeRegex(text: string): string {
16
+ return text.replace(REGEX_SPECIALS, '\\$&');
17
+ }
18
+
19
+ /**
20
+ * Convert a URL path into a RegExp source string, turning dynamic route
21
+ * placeholders (`:name`) into `[^/]+` wildcards while escaping literal segments.
22
+ * `/` is emitted as the escaped `\/` so the result drops straight into `/…/`.
23
+ */
24
+ export function pathToRegexSource(path: string): string {
25
+ return path
26
+ .split('/')
27
+ .map((segment) =>
28
+ /^:[A-Za-z_][A-Za-z0-9_]*$/.test(segment) ? '[^/]+' : escapeRegex(segment)
29
+ )
30
+ .join('\\/');
31
+ }