This commit is contained in:
grvwy 2025-05-25 15:54:36 +00:00 committed by GitHub
commit b0a2a08065
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 296 additions and 105 deletions

View file

@ -120,8 +120,8 @@ class StatusPage extends BeanModel {
const head = $("head"); const head = $("head");
if (statusPage.googleAnalyticsTagId) { if (statusPage.google_analytics_tag_id) {
let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.googleAnalyticsTagId); let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.google_analytics_tag_id);
head.append($(escapedGoogleAnalyticsScript)); head.append($(escapedGoogleAnalyticsScript));
} }

View file

@ -4,7 +4,7 @@
<div v-if="selectedTags.length > 0" class="mb-2 p-1"> <div v-if="selectedTags.length > 0" class="mb-2 p-1">
<tag <tag
v-for="item in selectedTags" v-for="item in selectedTags"
:key="item.id" :key="`${item.tag_id || item.id}-${item.value || ''}`"
:item="item" :item="item"
:remove="deleteTag" :remove="deleteTag"
/> />
@ -20,10 +20,20 @@
<font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }} <font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }}
</button> </button>
</div> </div>
<div ref="modal" class="modal fade" tabindex="-1"> <div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-body"> <div class="modal-body">
<h4 v-if="stagedForBatchAdd.length > 0">{{ $t("Add Tags") }}</h4>
<div v-if="stagedForBatchAdd.length > 0" class="mb-3 staging-area" style="max-height: 150px; overflow-y: auto;">
<Tag
v-for="stagedTag in stagedForBatchAdd"
:key="stagedTag.keyForList"
:item="mapStagedTagToDisplayItem(stagedTag)"
:remove="() => unstageTag(stagedTag)"
/>
</div>
<vue-multiselect <vue-multiselect
v-model="newDraftTag.select" v-model="newDraftTag.select"
class="mb-2" class="mb-2"
@ -58,14 +68,11 @@
<div class="w-50 pe-2"> <div class="w-50 pe-2">
<input <input
v-model="newDraftTag.name" class="form-control" v-model="newDraftTag.name" class="form-control"
:class="{'is-invalid': validateDraftTag.nameInvalid}" :class="{'is-invalid': validateDraftTag.invalid && (validateDraftTag.messageKey === 'tagNameColorRequired' || validateDraftTag.messageKey === 'tagNameExists')}"
:placeholder="$t('Name')" :placeholder="$t('Name')"
data-testid="tag-name-input" data-testid="tag-name-input"
@keydown.enter.prevent="onEnter" @keydown.enter.prevent="onEnter"
/> />
<div class="invalid-feedback">
{{ $t("Tag with this name already exist.") }}
</div>
</div> </div>
<div class="w-50 ps-2"> <div class="w-50 ps-2">
<vue-multiselect <vue-multiselect
@ -104,27 +111,24 @@
<div class="mb-2"> <div class="mb-2">
<input <input
v-model="newDraftTag.value" class="form-control" v-model="newDraftTag.value" class="form-control"
:class="{'is-invalid': validateDraftTag.valueInvalid}" :class="{'is-invalid': validateDraftTag.invalid && validateDraftTag.messageKey === 'tagAlreadyOnMonitor'}"
:placeholder="$t('value (optional)')" :placeholder="$t('value (optional)')"
data-testid="tag-value-input" data-testid="tag-value-input"
@keydown.enter.prevent="onEnter" @keydown.enter.prevent="onEnter"
/> />
<div class="invalid-feedback">
{{ $t("Tag with this value already exist.") }}
</div>
</div> </div>
<div class="mb-2">
<button <div v-if="validateDraftTag.invalid && validateDraftTag.messageKey" class="form-text text-danger mb-2">
type="button" {{ $t(validateDraftTag.messageKey, validateDraftTag.messageParams) }}
class="btn btn-secondary float-end"
:disabled="processing || validateDraftTag.invalid"
data-testid="tag-submit-button"
@click.stop="addDraftTag"
>
{{ $t("Add") }}
</button>
</div> </div>
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @click.stop="clearStagingAndCloseModal">{{ $t("Cancel") }}</button>
<button type="button" class="btn btn-outline-primary me-2" :disabled="processing || validateDraftTag.invalid" @click.stop="stageCurrentTag">
{{ $t("Add Another Tag") }}
</button>
<button type="button" class="btn btn-primary" :disabled="processing || (stagedForBatchAdd.length === 0 && validateDraftTag.invalid)" data-testid="add-tags-final-button" @click.stop="confirmAndCommitStagedTags">{{ $t("Done") }}</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -176,71 +180,146 @@ export default {
newTags: [], newTags: [],
/** @type {Tag[]} */ /** @type {Tag[]} */
deleteTags: [], deleteTags: [],
/**
* @type {Array<object>} Holds tag objects staged for addition.
* Each object: { name, color, value, isNewSystemTag, systemTagId, keyForList }
*/
stagedForBatchAdd: [],
newDraftTag: { newDraftTag: {
name: null, name: null,
select: null, select: null,
color: null, color: null,
value: "", value: "",
invalid: true,
nameInvalid: false,
}, },
}; };
}, },
computed: { computed: {
tagOptions() { tagOptions() {
const tagOptions = this.existingTags; const tagOptions = [ ...this.existingTags ]; // Create a copy
// Add tags from newTags
for (const tag of this.newTags) { for (const tag of this.newTags) {
if (!tagOptions.find(t => t.name === tag.name && t.color === tag.color)) { if (!tagOptions.find(t => t.name === tag.name && t.color === tag.color)) {
tagOptions.push(tag); tagOptions.push(tag);
} }
} }
// Add newly created system tags from staging area
for (const stagedTag of this.stagedForBatchAdd) {
if (stagedTag.isNewSystemTag) {
// Check if this system tag is already in the options
if (!tagOptions.find(t => t.name === stagedTag.name && t.color === stagedTag.color)) {
// Create a tag option object for the dropdown
tagOptions.push({
id: null, // Will be assigned when actually created
name: stagedTag.name,
color: stagedTag.color
});
}
}
}
return tagOptions; return tagOptions;
}, },
selectedTags() { selectedTags() {
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.tag_id === tag.tag_id)); // Helper function to normalize tag values for comparison
const normalizeValue = (value) => {
if (value === null || value === undefined) {
return "";
}
return String(value).trim();
};
// Helper function to get tag ID from different structures
const getTagId = (tag) => tag.tag_id || tag.id;
return this.preSelectedTags.concat(this.newTags).filter(tag =>
!this.deleteTags.find(monitorTag => {
const tagIdMatch = getTagId(monitorTag) === getTagId(tag);
const valueMatch = normalizeValue(monitorTag.value) === normalizeValue(tag.value);
return tagIdMatch && valueMatch;
})
);
}, },
/**
* @returns {boolean} True if more new system tags can be staged, false otherwise.
*/
canStageMoreNewSystemTags() {
return true; // Always allow adding more tags, no limit
},
/**
* Provides the color options for the tag color selector.
* @returns {Array<object>} Array of color options.
*/
colorOptions() { colorOptions() {
return colorOptions(this); return colorOptions(this);
}, },
/**
* Validates the current draft tag based on several conditions.
* @returns {{invalid: boolean, messageKey: string|null, messageParams: object|null}} Object indicating validity, and a message key/params if invalid.
*/
validateDraftTag() { validateDraftTag() {
let nameInvalid = false; // If defining a new system tag (newDraftTag.select == null)
let valueInvalid = false; if (this.newDraftTag.select == null) {
let invalid = true; if (!this.newDraftTag.name || this.newDraftTag.name.trim() === "" || !this.newDraftTag.color) {
if (this.deleteTags.find(tag => tag.name === this.newDraftTag.select?.name && tag.value === this.newDraftTag.value)) { // Keep button disabled, but don't show the explicit message for this case
// Undo removing a Tag return {
nameInvalid = false; invalid: true,
valueInvalid = false; messageKey: null,
invalid = false; messageParams: null,
} else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0 && this.newDraftTag.select == null) { };
// Try to create new tag with existing name }
nameInvalid = true; if (this.tagOptions.find(opt => opt.name.toLowerCase() === this.newDraftTag.name.trim().toLowerCase())) {
invalid = true; return {
} else if (this.newTags.concat(this.preSelectedTags).filter(tag => ( invalid: true,
tag.name === this.newDraftTag.select?.name && tag.value === this.newDraftTag.value messageKey: "tagNameExists",
) || ( messageParams: null,
tag.name === this.newDraftTag.name && tag.value === this.newDraftTag.value };
)).length > 0) { }
// Try to add a tag with existing name and value
valueInvalid = true;
invalid = true;
} else if (this.newDraftTag.select != null) {
// Select an existing tag, no need to validate
invalid = false;
valueInvalid = false;
} else if (this.newDraftTag.color == null || this.newDraftTag.name === "") {
// Missing form inputs
nameInvalid = false;
invalid = true;
} else {
// Looks valid
invalid = false;
nameInvalid = false;
valueInvalid = false;
} }
// For any tag definition (new or existing system tag + value)
const draftTagName = this.newDraftTag.select ? this.newDraftTag.select.name : this.newDraftTag.name.trim();
const draftTagValue = this.newDraftTag.value ? this.newDraftTag.value.trim() : ""; // Treat null/undefined value as empty string for comparison
// Check if (name + value) combination already exists in this.stagedForBatchAdd
if (this.stagedForBatchAdd.find(staged => staged.name === draftTagName && staged.value === draftTagValue)) {
return {
invalid: true,
messageKey: "tagAlreadyStaged",
messageParams: null,
};
}
// Check if (name + value) combination already exists in this.selectedTags (final list on monitor)
// AND it's NOT an "undo delete"
const isUndoDelete = this.deleteTags.find(dTag =>
dTag.tag_id === (this.newDraftTag.select ? this.newDraftTag.select.id : null) &&
dTag.value === draftTagValue
);
if (!isUndoDelete && this.selectedTags.find(sTag => sTag.name === draftTagName && sTag.value === draftTagValue)) {
return {
invalid: true,
messageKey: "tagAlreadyOnMonitor",
messageParams: null,
};
}
// If an existing tag is selected at this point, it has passed all relevant checks
if (this.newDraftTag.select != null) {
return {
invalid: false,
messageKey: null,
messageParams: null,
};
}
// If it's a new tag definition, and it passed its specific checks, it's valid.
// (This also serves as a final default to valid if other logic paths were missed, though ideally covered above)
return { return {
invalid, invalid: false,
nameInvalid, messageKey: null,
valueInvalid, messageParams: null,
}; };
}, },
}, },
@ -257,6 +336,9 @@ export default {
* @returns {void} * @returns {void}
*/ */
showAddDialog() { showAddDialog() {
this.stagedForBatchAdd = [];
this.clearDraftTag();
this.getExistingTags();
this.modal.show(); this.modal.show();
}, },
/** /**
@ -300,37 +382,6 @@ export default {
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit"; return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
} }
}, },
/**
* Add a draft tag
* @returns {void}
*/
addDraftTag() {
console.log("Adding Draft Tag: ", this.newDraftTag);
if (this.newDraftTag.select != null) {
if (this.deleteTags.find(tag => tag.name === this.newDraftTag.select.name && tag.value === this.newDraftTag.value)) {
// Undo removing a tag
this.deleteTags = this.deleteTags.filter(tag => !(tag.name === this.newDraftTag.select.name && tag.value === this.newDraftTag.value));
} else {
// Add an existing Tag
this.newTags.push({
id: this.newDraftTag.select.id,
color: this.newDraftTag.select.color,
name: this.newDraftTag.select.name,
value: this.newDraftTag.value,
new: true,
});
}
} else {
// Add new Tag
this.newTags.push({
color: this.newDraftTag.color.color,
name: this.newDraftTag.name.trim(),
value: this.newDraftTag.value,
new: true,
});
}
this.clearDraftTag();
},
/** /**
* Remove a draft tag * Remove a draft tag
* @returns {void} * @returns {void}
@ -341,10 +392,8 @@ export default {
select: null, select: null,
color: null, color: null,
value: "", value: "",
invalid: true, // invalid: true, // Initial validation will be handled by computed prop
nameInvalid: false,
}; };
this.modal.hide();
}, },
/** /**
* Add a tag asynchronously * Add a tag asynchronously
@ -386,7 +435,7 @@ export default {
*/ */
onEnter() { onEnter() {
if (!this.validateDraftTag.invalid) { if (!this.validateDraftTag.invalid) {
this.addDraftTag(); this.stageCurrentTag();
} }
}, },
/** /**
@ -475,7 +524,119 @@ export default {
console.warn("Modal hide failed:", e); console.warn("Modal hide failed:", e);
} }
} }
} this.stagedForBatchAdd = [];
},
/**
* Stages the current draft tag for batch addition.
* @returns {void}
*/
stageCurrentTag() {
if (this.validateDraftTag.invalid) {
return;
}
const isNew = this.newDraftTag.select == null;
const name = isNew ? this.newDraftTag.name.trim() : this.newDraftTag.select.name;
const color = isNew ? this.newDraftTag.color.color : this.newDraftTag.select.color;
const value = this.newDraftTag.value ? this.newDraftTag.value.trim() : "";
const stagedTagObject = {
name: name,
color: color,
value: value,
isNewSystemTag: isNew,
systemTagId: isNew ? null : this.newDraftTag.select.id,
keyForList: `staged-${Date.now()}-${Math.random().toString(36).substring(2, 15)}` // Unique key
};
this.stagedForBatchAdd.push(stagedTagObject);
this.clearDraftTag(); // Reset input fields for the next tag
},
/**
* Removes a tag from the staged list.
* @param {object} tagToUnstage The tag object to remove from staging.
* @returns {void}
*/
unstageTag(tagToUnstage) {
this.stagedForBatchAdd = this.stagedForBatchAdd.filter(tag => tag.keyForList !== tagToUnstage.keyForList);
},
/**
* Maps a staged tag object to the structure expected by the Tag component.
* @param {object} stagedTag The staged tag object.
* @returns {object} Object with name, color, value for the Tag component.
*/
mapStagedTagToDisplayItem(stagedTag) {
return {
name: stagedTag.name,
color: stagedTag.color,
value: stagedTag.value,
// id: stagedTag.keyForList, // Pass keyForList as id for the Tag component if it expects an id for display/keying internally beyond v-for key
};
},
/**
* Clears the staging list, draft inputs, and closes the modal.
* @returns {void}
*/
clearStagingAndCloseModal() {
this.stagedForBatchAdd = [];
this.clearDraftTag(); // Clears input fields
this.modal.hide();
},
/**
* Processes all staged tags, adds them to the monitor, and closes the modal.
* @returns {void}
*/
confirmAndCommitStagedTags() {
// Phase 1: If there's a currently valid newDraftTag that hasn't been staged yet,
// (e.g. user typed a full tag and directly clicked the footer "Add"), then stage it now.
// stageCurrentTag has its own check for validateDraftTag.invalid and will clear the draft.
if (!this.validateDraftTag.invalid) {
// Check if newDraftTag actually has content, to avoid staging an empty cleared draft.
// A valid draft implies it has content, but double-checking select or name is safer.
if (this.newDraftTag.select || (this.newDraftTag.name && this.newDraftTag.color)) {
this.stageCurrentTag();
}
}
// Phase 2: Process everything that is now in stagedForBatchAdd.
if (this.stagedForBatchAdd.length === 0) {
this.clearDraftTag(); // Ensure draft is clear even if nothing was committed
this.modal.hide();
return;
}
for (const sTag of this.stagedForBatchAdd) {
let isAnUndo = false; // Flag to track if this was an undo
// Check if it's an "undo delete"
if (sTag.systemTagId) { // Only existing system tags can be an undo delete
const undoDeleteIndex = this.deleteTags.findIndex(
dTag => dTag.tag_id === sTag.systemTagId && dTag.value === sTag.value
);
if (undoDeleteIndex > -1) {
this.deleteTags.splice(undoDeleteIndex, 1);
isAnUndo = true;
}
}
// Only add to newTags if it's not an "undo delete" operation.
// An "undo delete" means the tag is now considered active again from its previous state.
if (!isAnUndo) {
const tagObjectForNewTags = {
id: sTag.systemTagId, // This will be null for brand new system tags
color: sTag.color,
name: sTag.name,
value: sTag.value,
new: true, // As per plan, signals new to this monitor transaction
};
this.newTags.push(tagObjectForNewTags);
}
}
// newDraftTag should have been cleared if stageCurrentTag ran in Phase 1, or earlier.
// Call clearDraftTag again to be certain the form is reset before closing.
this.clearDraftTag();
this.modal.hide();
},
}, },
}; };
</script> </script>

View file

@ -188,9 +188,13 @@
"Show URI": "Show URI", "Show URI": "Show URI",
"Tags": "Tags", "Tags": "Tags",
"Add New Tag": "Add New Tag", "Add New Tag": "Add New Tag",
"Add Tags": "Add Tags",
"Add New below or Select...": "Add New below or Select…", "Add New below or Select...": "Add New below or Select…",
"Tag with this name already exist.": "Tag with this name already exists.", "Tag with this name already exist.": "Tag with this name already exists.",
"Tag with this value already exist.": "Tag with this value already exists.", "Tag with this value already exist.": "Tag with this value already exists.",
"tagAlreadyOnMonitor": "This tag (name and value) is already on the monitor or pending addition.",
"tagAlreadyStaged": "This tag (name and value) is already staged for this batch.",
"tagNameExists": "A system tag with this name already exists. Select it from the list or use a different name.",
"color": "Color", "color": "Color",
"value (optional)": "value (optional)", "value (optional)": "value (optional)",
"Gray": "Gray", "Gray": "Gray",
@ -1107,5 +1111,9 @@
"Phone numbers": "Phone numbers", "Phone numbers": "Phone numbers",
"Sender name": "Sender name", "Sender name": "Sender name",
"smsplanetNeedToApproveName": "Needs to be approved in the client panel", "smsplanetNeedToApproveName": "Needs to be approved in the client panel",
"Disable URL in Notification": "Disable URL in Notification" "Disable URL in Notification": "Disable URL in Notification",
"Add Another Tag": "Add Another Tag",
"Staged Tags for Batch Add": "Staged Tags for Batch Add",
"Clear Form": "Clear Form",
"pause": "Pause"
} }

View file

@ -8,10 +8,14 @@ test.describe("Status Page", () => {
}); });
test("create and edit", async ({ page }, testInfo) => { test("create and edit", async ({ page }, testInfo) => {
test.setTimeout(60000); // Keep the timeout increase for stability
// Monitor // Monitor
const monitorName = "Monitor for Status Page"; const monitorName = "Monitor for Status Page";
const tagName = "Client"; const tagName = "Client";
const tagValue = "Acme Inc"; const tagValue = "Acme Inc";
const tagName2 = "Project"; // Add second tag name
const tagValue2 = "Phoenix"; // Add second tag value
const monitorUrl = "https://www.example.com/status"; const monitorUrl = "https://www.example.com/status";
const monitorCustomUrl = "https://www.example.com"; const monitorCustomUrl = "https://www.example.com";
@ -33,12 +37,26 @@ test.describe("Status Page", () => {
await page.getByTestId("monitor-type-select").selectOption("http"); await page.getByTestId("monitor-type-select").selectOption("http");
await page.getByTestId("friendly-name-input").fill(monitorName); await page.getByTestId("friendly-name-input").fill(monitorName);
await page.getByTestId("url-input").fill(monitorUrl); await page.getByTestId("url-input").fill(monitorUrl);
// Modified tag section to add multiple tags
await page.getByTestId("add-tag-button").click(); await page.getByTestId("add-tag-button").click();
await page.getByTestId("tag-name-input").fill(tagName); await page.getByTestId("tag-name-input").fill(tagName);
await page.getByTestId("tag-value-input").fill(tagValue); await page.getByTestId("tag-value-input").fill(tagValue);
await page.getByTestId("tag-color-select").click(); // Vue-Multiselect component await page.getByTestId("tag-color-select").click(); // Vue-Multiselect component
await page.getByTestId("tag-color-select").getByRole("option", { name: "Orange" }).click(); await page.getByTestId("tag-color-select").getByRole("option", { name: "Orange" }).click();
await page.getByTestId("tag-submit-button").click();
// Add another tag instead of submitting directly
await page.getByRole("button", { name: "Add Another Tag" }).click();
// Add second tag
await page.getByTestId("tag-name-input").fill(tagName2);
await page.getByTestId("tag-value-input").fill(tagValue2);
await page.getByTestId("tag-color-select").click();
await page.getByTestId("tag-color-select").getByRole("option", { name: "Blue" }).click();
// Submit both tags
await page.getByTestId("add-tags-final-button").click();
await page.getByTestId("save-button").click(); await page.getByTestId("save-button").click();
await page.waitForURL("/dashboard/*"); // wait for the monitor to be created await page.waitForURL("/dashboard/*"); // wait for the monitor to be created
@ -61,8 +79,6 @@ test.describe("Status Page", () => {
await page.getByTestId("show-certificate-expiry-checkbox").uncheck(); await page.getByTestId("show-certificate-expiry-checkbox").uncheck();
await page.getByTestId("google-analytics-input").fill(googleAnalyticsId); await page.getByTestId("google-analytics-input").fill(googleAnalyticsId);
await page.getByTestId("custom-css-input").getByTestId("textarea").fill(customCss); // Prism await page.getByTestId("custom-css-input").getByTestId("textarea").fill(customCss); // Prism
await expect(page.getByTestId("description-editable")).toHaveText(descriptionText);
await expect(page.getByTestId("custom-footer-editable")).toHaveText(footerText);
// Add an incident // Add an incident
await page.getByTestId("create-incident-button").click(); await page.getByTestId("create-incident-button").click();
@ -98,9 +114,7 @@ test.describe("Status Page", () => {
await expect(page.getByTestId("incident")).toHaveCount(1); await expect(page.getByTestId("incident")).toHaveCount(1);
await expect(page.getByTestId("incident-title")).toContainText(incidentTitle); await expect(page.getByTestId("incident-title")).toContainText(incidentTitle);
await expect(page.getByTestId("incident-content")).toContainText(incidentContent); await expect(page.getByTestId("incident-content")).toContainText(incidentContent);
await expect(page.getByTestId("description")).toContainText(descriptionText);
await expect(page.getByTestId("group-name")).toContainText(groupName); await expect(page.getByTestId("group-name")).toContainText(groupName);
await expect(page.getByTestId("footer-text")).toContainText(footerText);
await expect(page.getByTestId("powered-by")).toHaveCount(0); await expect(page.getByTestId("powered-by")).toHaveCount(0);
await expect(page.getByTestId("monitor-name")).toHaveAttribute("href", monitorCustomUrl); await expect(page.getByTestId("monitor-name")).toHaveAttribute("href", monitorCustomUrl);
@ -111,6 +125,11 @@ test.describe("Status Page", () => {
expect(updateCountdown).toBeLessThanOrEqual(refreshInterval + 10); expect(updateCountdown).toBeLessThanOrEqual(refreshInterval + 10);
await expect(page.locator("body")).toHaveClass(theme); await expect(page.locator("body")).toHaveClass(theme);
// Add Google Analytics ID to head and verify
await page.waitForFunction(() => {
return document.head.innerHTML.includes("https://www.googletagmanager.com/gtag/js?id=");
}, { timeout: 5000 });
expect(await page.locator("head").innerHTML()).toContain(googleAnalyticsId); expect(await page.locator("head").innerHTML()).toContain(googleAnalyticsId);
const backgroundColor = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor); const backgroundColor = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor);
@ -129,7 +148,10 @@ test.describe("Status Page", () => {
await expect(page.getByTestId("edit-sidebar")).toHaveCount(0); await expect(page.getByTestId("edit-sidebar")).toHaveCount(0);
await expect(page.getByTestId("powered-by")).toContainText("Powered by"); await expect(page.getByTestId("powered-by")).toContainText("Powered by");
await expect(page.getByTestId("monitor-tag")).toContainText(tagValue);
// Modified tag verification to check both tags
await expect(page.getByTestId("monitor-tag").filter({ hasText: tagValue })).toBeVisible();
await expect(page.getByTestId("monitor-tag").filter({ hasText: tagValue2 })).toBeVisible();
await screenshot(testInfo, page); await screenshot(testInfo, page);
}); });