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
37 changes: 20 additions & 17 deletions E2E_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,41 @@ FlexiRule uses Cypress for end-to-end testing, following the Frappe Framework's
## Project Structure

- `cypress/`: Main Cypress directory.
- `integration/`: Contains test scripts (e.g., `rule_builder.js`).
- `support/`: Custom commands and global configuration.
- `fixtures/`: Static data for tests.
- `plugins/`: Cypress plugins.
- `integration/`: Contains test scripts (e.g., `rule_builder.js`).
- `support/`: Custom commands and global configuration.
- `fixtures/`: Static data for tests.
- `plugins/`: Cypress plugins.
- `cypress.config.js`: Cypress configuration file.

## Execution Flow

To run FlexiRule UI tests, follow these steps:

1. **Prepare a Test Site**:
```bash
bench new-site test_site
bench --site test_site install-app flexirule
bench --site test_site migrate
bench --site test_site execute frappe.utils.install.complete_setup_wizard
bench --site test_site set-admin-password admin
```

```bash
bench new-site test_site
bench --site test_site install-app flexirule
bench --site test_site migrate
bench --site test_site execute frappe.utils.install.complete_setup_wizard
bench --site test_site set-admin-password admin
```

2. **Start the Bench**:
```bash
bench --site test_site serve
```

```bash
bench --site test_site serve
```

3. **Run UI Tests**:
```bash
bench --site test_site run-ui-tests flexirule --headless
```
```bash
bench --site test_site run-ui-tests flexirule --headless
```

## Infrastructure Reuse

FlexiRule reuses Frappe's Cypress infrastructure (commands, helpers, utilities). To ensure stability and avoid cross-app pathing issues during execution, the core Frappe support scripts are copied into the FlexiRule repository:

- `cypress/support/frappe_commands.js`
- `cypress/support/frappe_e2e.js`

Expand Down
21 changes: 16 additions & 5 deletions e2e/HARDENING_REPORT.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,61 @@
# E2E Test Hardening Report

## Decision Summary

The objective was to verify the Playwright E2E test against the actual UI implementation and harden it against flakiness and architectural changes (like the migration to Action Type records) without modifying the application's source code.

## 9-Point Verification Checklist

### 1. Trigger Event Field

- **Finding**: Verified as a native HTML `<select>` inside a Frappe field wrapper.
- **Action**: Updated the selector to explicitly target the `select` element within the `[data-fieldname="trigger_event"]` container.

### 2. Document Type Link Field

- **Finding**: Verified as a standard Frappe Link field.
- **Action**: Hardened the interaction by using `fill` + `keyboard.press('Enter')` followed by a deterministic `expect(docTypeInput).toHaveValue('Contact')` assertion to ensure the async lookup completes.

### 3. Fixed Timeout Usage

- **Finding**: Multiple `waitForTimeout` were present in the initial draft.
- **Action**: Replaced with deterministic waits:
- `waitForResponse` for API calls (`savedocs`, `login`).
- `expect(...).toBeVisible()` for UI transitions.
- `page.waitForURL` for route changes.
- `waitForResponse` for API calls (`savedocs`, `login`).
- `expect(...).toBeVisible()` for UI transitions.
- `page.waitForURL` for route changes.

### 4. Action Node Selection

- **Finding**: Using `:has-text` on generic classes was risky.
- **Action**: Leveraged auto-generated CSS classes on nodes (e.g., `.assignment`, `.notify`) which correspond to the internal action type, providing unique and stable targeting on the canvas.

### 5. Checkbox Handling

- **Finding**: Initial script silently skipped missing fields.
- **Action**: Converted to explicit `expect(checkbox).toBeVisible()` and `expect(checkbox).toBeChecked()` assertions to enforce that mandatory configuration fields for specific action types are rendered as expected.

### 6. Save Verification

- **Finding**: Verification was based on a fixed wait.
- **Action**: Implemented dual-layer verification:
- Monitoring the `savedocs` API response status.
- Asserting the visibility of the "Saved" success toast (desk-alert) in the UI.
- Monitoring the `savedocs` API response status.
- Asserting the visibility of the "Saved" success toast (desk-alert) in the UI.

### 7. Post-Configuration Assertions

- **Finding**: No verification of canvas updates.
- **Action**: Added assertions to verify that updates in the `ActionSettings` sidebar (like `action_label`) correctly propagate to the canvas node titles (via `InlineEditor`).

### 8. Screenshot Path

- **Finding**: Directory existence was not guaranteed.
- **Action**: Added `fs.mkdirSync` in `test.beforeAll` to ensure the path exists in any execution environment.

### 9. Action Type Architecture Compatibility

- **Finding**: Migration to Action Type records may change display labels.
- **Action**: Per user feedback, avoided modifying source code to add `data-testid`. Instead, hardened the test by using stable internal selectors like `.result-item` and internal-name classes on nodes, which are more resilient than display text alone.

## Summary of Improvements

The hardened test is significantly more reliable as it removes artificial delays and replaces them with event-driven synchronization. It now explicitly catches regressions in field rendering and ensures that data persistence is verified at both the API and UI levels.
18 changes: 13 additions & 5 deletions flexirule/public/js/flexirule/core/control_registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,16 +189,24 @@ ControlRegistry.registerDefault({
// 8. MultiSelectList
ControlRegistry.registerDefault({
match: (df) =>
["MultiSelect", "MultiFieldPicker", "MultiSelectList", "MultiCheck"].includes(
df?.fieldtype
),
[
"MultiSelect",
"MultiFieldPicker",
"MultiSelectList",
"MultiCheck",
"Table MultiSelect",
].includes(df?.fieldtype),
component: "MultiSelectList",
mapProps: (df, context) => {
let displayMode = df.displayMode;
if (!displayMode) {
if (df.fieldtype === "MultiSelect" || df.fieldtype === "MultiFieldPicker")
if (
df.fieldtype === "MultiSelect" ||
df.fieldtype === "MultiFieldPicker" ||
df.fieldtype === "MultiSelectList" ||
df.fieldtype === "Table MultiSelect"
)
displayMode = "badges";
else if (df.fieldtype === "MultiSelectList") displayMode = "list";
else if (df.fieldtype === "MultiCheck") displayMode = "columns";
else displayMode = "badges";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,28 @@ window.frappe.query_report.set_filter_value = (name, val) => {
};
}
sync_local_config();

const filterDef = report_filters.value.find((f) => f.fieldname === name);
if (filterDef && typeof filterDef.on_change === "function") {
try {
filterDef.on_change();
} catch (e) {
console.warn(`Failed to run on_change for filter ${name}`, e);
}
}
};
window.frappe.query_report.toggle_filter_display = (fieldname, show) => {
const filterDef = report_filters.value.find((f) => f.fieldname === fieldname);
if (filterDef) {
filterDef.hidden = !show;
}
};
Object.defineProperty(window.frappe.query_report, "filters", {
get() {
return report_filters.value;
},
configurable: true,
});
window.cur_report = window.frappe.query_report;

const flat_report_filter_values = computed(() => {
Expand Down Expand Up @@ -1256,6 +1277,24 @@ async function load_report_filters(report_name) {
};
}
});

// Execute onload settings callback for initial filter adjustments
const settings = frappe.query_reports[report_name] || {};
if (typeof settings.onload === "function") {
try {
const mockReport = {
page: {
add_inner_button: () => {},
},
get_values: () => flat_report_filter_values.value,
set_filter_value: (name, val) =>
window.frappe.query_report.set_filter_value(name, val),
};
settings.onload(mockReport);
} catch (e) {
console.warn("Failed to execute onload handler for report", report_name, e);
}
}
} catch (e) {
console.error("Failed to load report filters", e);
} finally {
Expand All @@ -1266,6 +1305,15 @@ async function load_report_filters(report_name) {
function update_report_filter(fieldname, value) {
report_filter_values[fieldname] = value;
sync_local_config();

const filterDef = report_filters.value.find((f) => f.fieldname === fieldname);
if (filterDef && typeof filterDef.on_change === "function") {
try {
filterDef.on_change();
} catch (e) {
console.warn(`Failed to run on_change for filter ${fieldname}`, e);
}
}
}

function fetch_default_values() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,22 @@ const {
reset: resetOptionSource,
} = useAsyncOptionsSource(async ({ query: search, start, pageSize }) => {
if (props.get_query) {
const rows = await props.get_query(search, props.filters || {});
if (rows !== null) {
return metaStore.uniqueOptions(metaStore.normalizeLinkRows(rows || []));
const res = await props.get_query(search, props.filters || {});
if (res !== null) {
if (res && typeof res === "object" && !Array.isArray(res)) {
if (res.filters || res.query) {
const mergedFilters = { ...(props.filters || {}), ...(res.filters || {}) };
return await metaStore.search_link_options({
doctype: effectiveDoctype.value,
txt: search || "",
filters: mergedFilters,
start,
page_length: pageSize,
});
}
} else {
return metaStore.uniqueOptions(metaStore.normalizeLinkRows(res || []));
}
}
}
if (effectiveDoctype.value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,26 @@
:class="{
'is-dynamic': isDynamicMode || !isStaticSupported,
'is-static-link': !isDynamicMode && isLinkType,
'is-multi-select': !isDynamicMode && isMultiSelect,
}"
@click="onWrapClick"
>
<!-- Static Mode (via ControlFactory or MultiSelectList) -->
<div v-if="!isDynamicMode && isStaticSupported" class="fvc-static-container flex-1">
<div
v-if="!isDynamicMode && isStaticSupported"
class="fvc-static-container flex-1"
:class="{ 'is-multi-select': isMultiSelect }"
>
<MultiSelectList
v-if="isMultiSelect"
:df="staticDf"
:modelValue="staticValue"
:documentType="isLinkType ? referenceDoctype : undefined"
:options="isLinkType ? undefined : fieldOptions"
:get_data="props.context?.df?.get_data"
displayMode="badges"
:badgeCollapseAfter="2"
:allowWrap="false"
:badgeCollapseAfter="0"
:allowWrap="true"
:hideLabel="true"
class="flex-1 min-w-0 w-100"
@update:modelValue="updateStaticValue"
Expand Down Expand Up @@ -377,11 +383,16 @@ const emit = defineEmits(["update:modelValue", "update"]);

const isReadOnly = computed(() => !!props.readOnly || !!props.read_only);
const isMultiSelect = computed(() => {
const ft = fieldType.value;
// Always treat native multi-value fieldtypes as multi-select
if (["MultiSelectList", "MultiSelect", "Table MultiSelect"].includes(ft)) {
return true;
}

const op = props.context?.operator;
const isListOp = op === "in list" || op === "not in list";
if (!isListOp) return false;

const ft = fieldType.value;
return (
ft === "Select" || PURE_TEXT_FIELDTYPES.has(ft) || ft === "Link" || ft === "Dynamic Link"
);
Expand Down Expand Up @@ -1335,6 +1346,13 @@ onBeforeUnmount(() => {
transition: all 0.2s ease;
}

.fvc-main-field.is-multi-select {
height: auto !important;
min-height: var(--fxr-input-height, 32px) !important;
max-height: none !important;
overflow: visible !important;
}

.fvc-main-field:focus-within {
border-color: var(--fxr-accent);
box-shadow: var(--fxr-shadow-focus);
Expand Down Expand Up @@ -1366,6 +1384,15 @@ onBeforeUnmount(() => {
height: var(--fxr-input-height, 32px) !important;
}

.fvc-static-container.is-multi-select :deep(.multi-select-trigger) {
height: auto !important;
min-height: var(--fxr-input-height, 30px) !important;
border: none !important;
box-shadow: none !important;
background: transparent !important;
padding: 4px var(--fxr-input-padding-x) !important;
}

.fvc-main-field:focus-within {
border-color: var(--fxr-accent, #2490ef);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--fxr-accent, #2490ef) 20%, transparent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,17 @@ const selectedValues = computed(() => {
return [String(props.modelValue)];
});

const isRemote = computed(() =>
Boolean(props.get_data || props.documentType || props.df?.fieldtype === "Link")
);
const isRemote = computed(() => {
const ft = props.df?.fieldtype;
const isMultiLink = ["Link", "MultiSelect", "MultiSelectList", "Table MultiSelect"].includes(
ft
);
return Boolean(
props.get_data ||
props.documentType ||
(isMultiLink && props.df?.options && typeof props.df.options === "string")
);
});

const {
options: fetchedOptions,
Expand All @@ -111,7 +119,11 @@ const {
);
}

if (props.df?.fieldtype === "Link" && props.df?.options) {
const ft = props.df?.fieldtype;
const isMultiLink = ["Link", "MultiSelect", "MultiSelectList", "Table MultiSelect"].includes(
ft
);
if (isMultiLink && props.df?.options && typeof props.df.options === "string") {
return await metaStore.search_link_options({
doctype: props.df.options,
txt: search || "",
Expand Down
Loading
Loading