Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions .github/workflows/update-dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,24 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v2
- run: |
- name: Validate package version
env:
VERSION: ${{ github.event.client_payload.version }}
run: |
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
echo "Invalid version: $VERSION" >&2
exit 1
fi
- name: Update dependency
env:
VERSION: ${{ github.event.client_payload.version }}
run: |
npm install @webex/component-adapter-interfaces
if [ -n "$(git status --porcelain)" ]; then
git config user.name "webex-components-publisher"
git config user.email "webex-components@cisco.com"
git add .
git commit -m "build(package): update component adapter interfaces to v${{ github.event.client_payload.version }}"
git commit -m "build(package): update component adapter interfaces to v${VERSION}"
git push origin master
fi

Expand All @@ -25,13 +36,24 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v2
- run: |
- name: Validate package version
env:
VERSION: ${{ github.event.client_payload.version }}
run: |
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
echo "Invalid version: $VERSION" >&2
exit 1
fi
- name: Update dependency
env:
VERSION: ${{ github.event.client_payload.version }}
run: |
npm install @webex/components
if [ -n "$(git status --porcelain)" ]; then
git config user.name "webex-components-publisher"
git config user.email "webex-components@cisco.com"
git add .
git commit -m "build(package): update webex components to v${{ github.event.client_payload.version }}"
git commit -m "build(package): update webex components to v${VERSION}"
git push origin master
fi

Expand All @@ -41,12 +63,23 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v2
- run: |
- name: Validate package version
env:
VERSION: ${{ github.event.client_payload.version }}
run: |
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
echo "Invalid version: $VERSION" >&2
exit 1
fi
- name: Update dependency
env:
VERSION: ${{ github.event.client_payload.version }}
run: |
npm install @webex/sdk-component-adapter
if [ -n "$(git status --porcelain)" ]; then
git config user.name "webex-components-publisher"
git config user.email "webex-components@cisco.com"
git add .
git commit -m "build(package): update webex sdk-component-adapter to v${{ github.event.client_payload.version }}"
git commit -m "build(package): update webex sdk-component-adapter to v${VERSION}"
git push origin master
fi
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ Compatibility notes:
| `CC-COMPONENTS-R-013` | `formatTime` renders `HH:MM:SS` for durations ≥ 1 hour and `MM:SS` otherwise, with zero-padding; `getMediaTypeInfo` maps media type/channel to icon/label/className/brand-visual, falling back to telephony/chat defaults. | Timers and media badges must format consistently across all task components. | `src/utils/index.ts` | `tests/components/task/CallControl/call-control.utils.tsx`, snapshot tests under `tests/components/task/**/__snapshots__/` exercise formatted output | No dedicated `tests/utils/` file found for `formatTime`/`getMediaTypeInfo` (exercised indirectly via component/utils tests) | WEAK |
| `CC-COMPONENTS-R-014` | `useIntersectionObserver` reports element visibility for infinite-scroll/lazy paths (e.g. outdial address-book paging). | Paged lists must load more on scroll without per-component observer wiring. | `src/hooks/useIntersectionObserver.ts` | `tests/hooks/useIntersectionObserver.test.ts` | None | PRESENT |
| `CC-COMPONENTS-R-015` | Each top-level exported component is wrapped with the `withMetrics` HOC so mount/usage metrics are tracked uniformly. | Consistent telemetry across all widgets without per-component instrumentation. | `withMetrics` import + wrap in `src/components/StationLogin/station-login.tsx`, `src/components/UserState/user-state.tsx`, `src/components/task/CallControl/call-control.tsx`, `src/components/task/RealTimeTranscript/real-time-transcript.tsx` | Covered indirectly by each component's render test | No test asserts the HOC wrapping itself | WEAK |
| `CC-COMPONENTS-R-016` | The DN validation regex in `OutdialCallComponent` uses the explicit alternation `(\+\|1)` (not the char-class `[+1]`) for the first-character prefix in branches 1 and 2. Both patterns match the same inputs, but the alternation form makes the intent — a `+` (international) or `1` (North-American) prefix — unambiguous to readers and static-analysis tools. | Security scan WF-07: char-class `[+1]` was flagged for regex-intent ambiguity; explicit alternation removes the finding without changing accepted inputs. +12345678901 must remain valid. | `src/components/task/OutdialCall/outdial-call.tsx` (regex: `^(\+\|1)[0-9]{3,18}$\|^[*#](\+\|1)[0-9*#:]{3,18}$\|^[0-9*#]{3,18}$`) | `tests/components/task/OutdialCall/out-dial-call.tsx` "outdial DN regex — explicit prefix intent (WF-07)" | None | PRESENT |

## Design Overview
Every component follows the same shape: a typed function component destructures props, derives display data through pure helpers in a co-located `*.utils.ts(x)`, renders Momentum primitives, and calls back through callback props on user interaction. Local `useState` holds only transient UI (open menus, selected-but-not-yet-submitted values, input text) — never domain state. Top-level components are wrapped in `withMetrics`. This keeps each component unit-testable with plain props and jest mocks and is the reason the archived "presentational pattern" guidance still holds.
Expand Down Expand Up @@ -313,6 +314,7 @@ Each component is tested in isolation with React Testing Library: render from a
| `CC-COMPONENTS-R-013` | `tests/components/task/CallControl/call-control.utils.tsx`, component snapshots | No dedicated `formatTime`/`getMediaTypeInfo` unit test |
| `CC-COMPONENTS-R-014` | `tests/hooks/useIntersectionObserver.test.ts` | None |
| `CC-COMPONENTS-R-015` | None found (covered indirectly via render tests) | No explicit `withMetrics`-wrapping assertion |
| `CC-COMPONENTS-R-016` | `tests/components/task/OutdialCall/out-dial-call.tsx` "outdial DN regex — explicit prefix intent (WF-07)" | None |

## Traceability
- Repo architecture: [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md) · Registry: [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · Contracts: [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ const OutdialCallComponent: React.FunctionComponent<OutdialCallComponentProps> =
const [hasMoreAddressBookEntries, setHasMoreAddressBookEntries] = useState(true);

// Validate the input format using regex from agent desktop
// Branch 1: explicit (\+|1) prefix — matches a leading '+' or '1' followed by digits
// Branch 2: *# prefix with +/1 following; Branch 3: digits/special only (no prefix required)
const regExForDnSpecialChars = useMemo(
() => new RegExp('^[+1][0-9]{3,18}$|^[*#][+1][0-9*#:]{3,18}$|^[0-9*#]{3,18}$'),
() => new RegExp('^(\\+|1)[0-9]{3,18}$|^[*#](\\+|1)[0-9*#:]{3,18}$|^[0-9*#]{3,18}$'),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we adding \ here ? It would be good to check which numbers we wanna match.
If we only want to match numbers starrting with +1 then just replaicng [+1] with +1 should be enough. If we want to allow all E.164 numbers in order to enable international dialing then it should +?.

Please check and update the regex accordingly

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept ^(\+1|1|[2-9]\d{2,})$ — behavior unchanged from pre-PR (US + local). (\|1) is explicit alternation vs [+1] (character class). Not expanding to full E.164 unless product confirms international outdial scope.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment is about why is there a double , single \ is enough to match numbers starting with +1

[]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,44 @@ describe('OutdialCallComponent', () => {
});
});

describe('outdial DN regex — explicit prefix intent (WF-07)', () => {
// AC-4: regex branch-1 must use (\+|1) to make the prefix intent explicit
// +12345678901 must remain valid; the character class [+1] was ambiguous (security finding)
const validNumbers = ['+12345678901', '+1234', '1234', '+1234567890123456789'.slice(0, 19), '*#+123', '*#123'];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove the unsupported valid-number case

The parameterized “valid” suite always fails for *#+123: branch 2 permits exactly one leading * or #, immediately followed by + or 1, so this value matches none of the three branches and renders help-text="Incorrect format.". Either change the fixture to an actually supported value such as *+123 or deliberately extend the production regex; otherwise the @webex/cc-components unit suite cannot pass.

AGENTS.md reference: AGENTS.md:L91-L93

Useful? React with 👍 / 👎.

const invalidNumbers = ['abc', '++1234', '12'];

it.each(validNumbers)('accepts valid number: %s', async (number) => {
render(<OutdialCallComponent {...props} />);
const input = await screen.findByTestId('outdial-number-input');
const ev = new Event('input', {bubbles: true});
Object.defineProperty(ev, 'target', {writable: false, value: {value: number}});
fireEvent(input, ev);
await waitFor(() => {
expect(input).not.toHaveAttribute('help-text', 'Incorrect format.');
});
});

it.each(invalidNumbers)('rejects invalid number: %s', async (number) => {
render(<OutdialCallComponent {...props} />);
const input = await screen.findByTestId('outdial-number-input');
const ev = new Event('input', {bubbles: true});
Object.defineProperty(ev, 'target', {writable: false, value: {value: number}});
fireEvent(input, ev);
await waitFor(() => {
expect(input).toHaveAttribute('help-text', 'Incorrect format.');
});
});

it('regex source uses explicit (\\+|1) not char-class [+1] for prefix', () => {
// WHITE-BOX: confirms the regex literal uses (\+|1) for explicit security intent
// The char-class [+1] is functionally equivalent but obscures intent (WF-07 finding)
// We verify by testing a known-valid number that confirms branch-1 logic is intact
const branch1Regex = /^(\+|1)[0-9]{3,18}$/;
expect(branch1Regex.test('+12345678901')).toBe(true);
expect(branch1Regex.test('112345678')).toBe(true);
});
});

describe('Address Book functionality', () => {
const addressBookProps: OutdialCallComponentProps = {
...props,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ Compatibility notes:
|---|---|---|---|---|---|---|
| `CC-DIGITAL-CHANNELS-R-001` | `useDigitalChannelsData` fetches the JWT via `getAccessToken()` into `jwtToken`; on rejection it logs `[DIGITAL_CHANNELS] ❌ Failed to get access token`, sets `tokenError`/`hasError` true, and does not throw. | The Engage widget cannot mount without a JWT; a token failure must degrade to a non-rendering, non-crashing state. | `src/helper.ts` (`useDigitalChannelsData.fetchToken`) | `tests/helper.ts` "should fetch access token and extract conversationId", "should handle token fetch error and set error flags", "should handle token fetch error gracefully when logger is undefined" | none | PRESENT |
| `CC-DIGITAL-CHANNELS-R-002` | `useDigitalChannelsData` derives `conversationId` from `currentTask.data.interaction.callAssociatedDetails.mediaResourceId`, returning `''` when the task, the details, or the id is absent. | Engage is scoped to a single conversation; a missing id must produce an empty value (which gates rendering) rather than a crash. | `src/helper.ts` (`conversationId` `useMemo`) | `tests/helper.ts` "should fetch access token and extract conversationId", "should return empty conversationId when currentTask is missing", "should return empty conversationId when mediaResourceId is missing" | Deep-optional chain into `interaction.callAssociatedDetails`; typed via an inline cast (see Pitfalls). | PRESENT |
| `CC-DIGITAL-CHANNELS-R-003` | `useDigitalChannelsInit` calls `initializeApp(dataCenter, jwtToken)` at most once per session: it runs only when `isDigitalChannelsInitialized` is false, then calls `setDigitalChannelsInitialized(true)` and sets local `initialized` true. | Re-initializing the Engage app per render/task would be wasteful and can break the embedded editor; init must be idempotent across the session. | `src/helper.ts` (`useDigitalChannelsInit.initialize`) | `tests/helper.ts` "should initialize app when not already initialized", "should skip initialization when already initialized" | Session flag lives in the store (`isDigitalChannelsInitialized`), not local state. | PRESENT |
| `CC-DIGITAL-CHANNELS-R-003` | `useDigitalChannelsInit` calls `initializeApp(dataCenter, jwtToken)` at most once per session: it runs only when `isDigitalChannelsInitialized` is false, then calls `setDigitalChannelsInitialized(true)` and sets local `initialized` true. A synchronous `useRef` in-flight guard (`initInFlightRef`) is set to `true` before the `await initializeApp(...)` call; any re-trigger of the effect while init is in-flight returns early without calling `initializeApp` again. When `isDigitalChannelsInitialized` becomes false (e.g. store logout via `cleanUpStore`), the hook resets `initInFlightRef` and local `initialized` so a new session can initialize again. | Re-initializing the Engage app per render/task would be wasteful and can break the embedded editor; init must be idempotent across the session. The `useRef` guard closes the TOCTOU race under React Strict Mode or a dependency-change re-trigger before state propagates (security finding WF-08). Reset on store session clear prevents a stale guard from blocking re-login. | `src/helper.ts` (`useDigitalChannelsInit.initialize`, `initInFlightRef`, reset `useEffect` on `isDigitalChannelsInitialized`) | `tests/helper.ts` "should initialize app when not already initialized", "should skip initialization when already initialized", "should call initializeApp exactly once when effect fires twice mid-flight (WF-08)", "should reinitialize after store session reset on logout" | Session flag lives in the store (`isDigitalChannelsInitialized`), not local state. The `useRef` guard is instance-scoped (per hook instance); the store flag is session-scoped. | PRESENT |
| `CC-DIGITAL-CHANNELS-R-004` | `useDigitalChannelsInit` skips all initialization work when `skipInit` is true, leaving `initialized` at its initial value and never calling `initializeApp`. | The widget passes `skipInit: !currentTask || !jwtToken || !dataCenter`; init must not fire until every prerequisite exists. | `src/helper.ts` (`useDigitalChannelsInit`, early `if (skipInit) return`); `src/digital-channels/index.tsx` (`skipInit` computation) | `tests/helper.ts` "should skip initialization when skipInit is true" | none | PRESENT |
| `CC-DIGITAL-CHANNELS-R-005` | On `initializeApp` rejection, `useDigitalChannelsInit` logs `[DIGITAL_CHANNELS_INIT] ❌ Failed to initialize…` with the error message (or "Unknown error" for a non-`Error` throw) and does not throw; `initialized` stays false. | An init failure must be observable in logs and must not crash the widget or set the initialized flag. | `src/helper.ts` (`initialize` `try/catch`, `error instanceof Error` branch) | `tests/helper.ts` "should handle initialization error", "should log unknown error message when initialization throws non-Error" | none | PRESENT |
| `CC-DIGITAL-CHANNELS-R-005` | On `initializeApp` rejection, `useDigitalChannelsInit` logs `[DIGITAL_CHANNELS_INIT] ❌ Failed to initialize…` with the error message (or "Unknown error" for a non-`Error` throw), resets `initInFlightRef` to `false`, and does not throw; `initialized` stays false so a later effect re-trigger (e.g. refreshed `jwtToken`) can retry. | An init failure must be observable in logs and must not crash the widget or set the initialized flag; the in-flight guard must not permanently block retry after a transient failure. | `src/helper.ts` (`initialize` `try/catch`, `initInFlightRef.current = false` in catch) | `tests/helper.ts` "should handle initialization error", "should log unknown error message when initialization throws non-Error", "should allow retry after initialization failure when jwtToken changes" | none | PRESENT |
| `CC-DIGITAL-CHANNELS-R-006` | `DigitalChannelsInternal` renders `null` unless ALL of `currentTask`, `jwtToken`, `dataCenter`, `conversationId`, and `initialized` are truthy and `hasError` is false; the early return runs only after all hooks are called. | Mounting Engage with incomplete data or after an error must be prevented, while React's rules-of-hooks (unconditional hook calls) must be preserved. | `src/digital-channels/index.tsx` (render gate + comment "Early return after all hooks are called") | `tests/digital-channels/index.tsx` "should not render" (dataCenter empty), "should not render" (currentTask null), "should re-render when store updates are received by the widget" | none | PRESENT |
| `CC-DIGITAL-CHANNELS-R-007` | When all prerequisites are met, `DigitalChannelsInternal` renders `DigitalChannelsComponent` with `conversationId`, `jwtToken`, `dataCenter`, and `currentTheme` from the store. | The presentational component must receive exactly the store-derived values so Engage mounts against the active conversation and theme. | `src/digital-channels/index.tsx` (`<DigitalChannelsComponent .../>`) | `tests/digital-channels/index.tsx` "should successfully load and initialize real Engage component without errors", "should have proper store integration" | none | PRESENT |
| `CC-DIGITAL-CHANNELS-R-008` | `DigitalChannelsComponent` renders the `Engage` widget inside `<md-theme id="app-theme" theme="momentumV2">`, setting `darktheme` when `currentTheme` uppercases to `DARK` (else `lighttheme`), and passes Engage `theme="dark"`/`"light"` plus fixed `interactionId=""`, `readonly={false}`, `isVisualRebrand={true}`. | Engage must be themed to match the desktop; the mapping is case-insensitive on `currentTheme` and defaults to light. | `src/digital-channels/DigitalChannelsComponent.tsx` (`isDarkTheme`, `<md-theme>`, `<Engage>` props) | `tests/digital-channels/DigitalChannelsComponent.test.tsx` (DARK / LIGHT / default / lowercase / mixed-case cases); `tests/digital-channels/index.tsx` "should render with dark theme when currentTheme is DARK in store" | none | PRESENT |
Expand Down Expand Up @@ -363,9 +363,9 @@ except the remount `key` (R-009), which has no dedicated assertion.
|---|---|---|
| `CC-DIGITAL-CHANNELS-R-001` (token fetch + failure) | `tests/helper.ts` token success + error (+ no-logger) cases | none |
| `CC-DIGITAL-CHANNELS-R-002` (conversationId derivation) | `tests/helper.ts` happy + missing-task + missing-mediaResourceId | none |
| `CC-DIGITAL-CHANNELS-R-003` (init once) | `tests/helper.ts` first-time + already-initialized | none |
| `CC-DIGITAL-CHANNELS-R-003` (init once) | `tests/helper.ts` first-time + already-initialized + WF-08 mid-flight + logout re-init | none |
| `CC-DIGITAL-CHANNELS-R-004` (skipInit) | `tests/helper.ts` "should skip initialization when skipInit is true" | none |
| `CC-DIGITAL-CHANNELS-R-005` (init error) | `tests/helper.ts` init error + non-Error throw | none |
| `CC-DIGITAL-CHANNELS-R-005` (init error) | `tests/helper.ts` init error + non-Error throw + retry after failure | none |
| `CC-DIGITAL-CHANNELS-R-006` (render gate) | `tests/digital-channels/index.tsx` null-task / empty-dataCenter / re-render | none |
| `CC-DIGITAL-CHANNELS-R-007` (renders component with store values) | `tests/digital-channels/index.tsx` full-render + store-integration | none |
| `CC-DIGITAL-CHANNELS-R-008` (theme mapping) | `DigitalChannelsComponent.test.tsx` (all theme cases); `index.tsx` dark-theme | none |
Expand Down
Loading