Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
44 changes: 43 additions & 1 deletion server/monitor-types/real-browser-monitor-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,40 @@ class RealBrowserMonitorType extends MonitorType {
timeout: monitor.interval * 1000 * 0.8,
});

// Check for keyword if configured
if (monitor.keyword && monitor.keyword.trim()) {
try {
// Extract all visible text content from the page
let textContent = await page.textContent("body");

if (textContent) {
textContent = textContent.replace(/\s+/g, " ").trim();
let keywordFound = textContent.includes(monitor.keyword);
const invertKeyword = monitor.invertKeyword === true || monitor.invertKeyword === 1;

if (keywordFound === !invertKeyword) {
log.debug("monitor", `Keyword check passed. Keyword "${monitor.keyword}" ${keywordFound ? "found" : "not found"} on page (invert: ${invertKeyword})`);
} else {
let errorText = textContent;
if (errorText.length > 50) {
errorText = errorText.substring(0, 47) + "...";
}

throw new Error(
`Keyword check failed. Keyword "${monitor.keyword}" ${keywordFound ? "found" : "not found"} on page. ` +
`Expected: ${invertKeyword ? "not found" : "found"}. Page content: [${errorText}]`
);
}
} else {
throw new Error("Could not extract text content from page for keyword checking");
}
} catch (keywordError) {
// Close context before throwing error
await context.close();
throw keywordError;
}
}

let filename = jwt.sign(monitor.id, server.jwtSecret) + ".png";

await page.screenshot({
Expand All @@ -263,7 +297,15 @@ class RealBrowserMonitorType extends MonitorType {

if (res.status() >= 200 && res.status() < 400) {
heartbeat.status = UP;
heartbeat.msg = res.status();
let statusMsg = res.status().toString();

// Add keyword info to message if keyword checking was performed
if (monitor.keyword && monitor.keyword.trim()) {
const invertKeyword = monitor.invertKeyword === true || monitor.invertKeyword === 1;
statusMsg += `, keyword "${monitor.keyword}" ${invertKeyword ? "not found" : "found"}`;
}

heartbeat.msg = statusMsg;

const timing = res.request().timing();
heartbeat.ping = timing.responseEnd;
Expand Down
19 changes: 15 additions & 4 deletions src/pages/EditMonitor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,27 @@
</div>

<!-- Keyword -->
<div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword'" class="my-3">
<div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword' || monitor.type === 'real-browser'" class="my-3">
<label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
<input
id="keyword"
v-model="monitor.keyword"
type="text"
class="form-control"
:required="monitor.type === 'keyword' || monitor.type === 'grpc-keyword'"
>
<div class="form-text">
{{ $t("keywordDescription") }}
<span v-if="monitor.type === 'real-browser'">
{{ $t("keywordDescription") }} {{ $t("Optional") }}.
</span>
<span v-else>
{{ $t("keywordDescription") }}
</span>
</div>
</div>

<!-- Invert keyword -->
<div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword'" class="my-3 form-check">
<div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword' || monitor.type === 'real-browser'" class="my-3 form-check">
<input id="invert-keyword" v-model="monitor.invertKeyword" class="form-check-input" type="checkbox">
<label class="form-check-label" for="invert-keyword">
{{ $t("Invert Keyword") }}
Expand Down
114 changes: 114 additions & 0 deletions test/backend-test/test-real-browser-keyword.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
const test = require("node:test");
const assert = require("node:assert");

// Mock monitor configurations for testing
const monitors = {
noKeyword: {
type: "real-browser",
url: "https://example.com"
},
withKeyword: {
type: "real-browser",
url: "https://example.com",
keyword: "Hello",
invertKeyword: false
},
withInvertKeyword: {
type: "real-browser",
url: "https://example.com",
keyword: "NotFound",
invertKeyword: true
},
keywordNotFound: {
type: "real-browser",
url: "https://example.com",
keyword: "NotFound",
invertKeyword: false
}
};

test("Test keyword checking logic", async (t) => {

await t.test("should pass when no keyword is configured", async () => {
// This simulates backward compatibility - monitors without keywords should work
const monitor = monitors.noKeyword;
assert.strictEqual(monitor.keyword, undefined);
// No keyword means no checking, should always pass the keyword part
});

await t.test("should pass when keyword is found and not inverted", async () => {
const monitor = monitors.withKeyword;
const pageText = "Hello World! This is a test page with sample content.";

// Simulate the keyword checking logic
const keywordFound = pageText.includes(monitor.keyword);
const invertKeyword = monitor.invertKeyword === true || monitor.invertKeyword === 1;
const shouldPass = keywordFound === !invertKeyword;

assert.strictEqual(keywordFound, true, "Keyword 'Hello' should be found in page text");
assert.strictEqual(invertKeyword, false, "Invert keyword should be false");
assert.strictEqual(shouldPass, true, "Check should pass when keyword found and not inverted");
});

await t.test("should pass when keyword is not found and inverted", async () => {
const monitor = monitors.withInvertKeyword;
const pageText = "Hello World! This is a test page with sample content.";

// Simulate the keyword checking logic
const keywordFound = pageText.includes(monitor.keyword);
const invertKeyword = monitor.invertKeyword === true || monitor.invertKeyword === 1;
const shouldPass = keywordFound === !invertKeyword;

assert.strictEqual(keywordFound, false, "Keyword 'NotFound' should not be found in page text");
assert.strictEqual(invertKeyword, true, "Invert keyword should be true");
assert.strictEqual(shouldPass, true, "Check should pass when keyword not found and inverted");
});

await t.test("should fail when keyword is not found and not inverted", async () => {
const monitor = monitors.keywordNotFound;
const pageText = "Hello World! This is a test page with sample content.";

// Simulate the keyword checking logic
const keywordFound = pageText.includes(monitor.keyword);
const invertKeyword = monitor.invertKeyword === true || monitor.invertKeyword === 1;
const shouldPass = keywordFound === !invertKeyword;

assert.strictEqual(keywordFound, false, "Keyword 'NotFound' should not be found in page text");
assert.strictEqual(invertKeyword, false, "Invert keyword should be false");
assert.strictEqual(shouldPass, false, "Check should fail when keyword not found and not inverted");
});

await t.test("should handle empty keyword properly", async () => {
const monitor = {
type: "real-browser",
url: "https://example.com",
keyword: "", // Empty keyword
invertKeyword: false
};

// Empty or whitespace-only keywords should be ignored (treated as no keyword)
const shouldCheckKeyword = !!(monitor.keyword && monitor.keyword.trim());
assert.strictEqual(shouldCheckKeyword, false, "Empty keyword should be ignored");
});

await t.test("should handle whitespace-only keyword properly", async () => {
const monitor = {
type: "real-browser",
url: "https://example.com",
keyword: " ", // Whitespace-only keyword
invertKeyword: false
};

// Empty or whitespace-only keywords should be ignored (treated as no keyword)
const shouldCheckKeyword = !!(monitor.keyword && monitor.keyword.trim());
assert.strictEqual(shouldCheckKeyword, false, "Whitespace-only keyword should be ignored");
});

await t.test("should handle text preprocessing correctly", async () => {
const originalText = "Hello World!\n\n This is a test page.";
const processedText = originalText.replace(/\s+/g, " ").trim();
const expectedText = "Hello World! This is a test page.";

assert.strictEqual(processedText, expectedText, "Text should be properly preprocessed");
});
});
84 changes: 84 additions & 0 deletions test/e2e/specs/monitor-form.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,88 @@ test.describe("Monitor Form", () => {

await screenshot(testInfo, page);
});

test("real-browser monitor with keyword functionality", async ({ page }, testInfo) => {
await page.goto("./add");
await login(page);
await screenshot(testInfo, page);

// Select real-browser monitor type
await selectMonitorType(page, "real-browser");

const friendlyName = "Test Real Browser with Keyword";
await page.getByTestId("friendly-name-input").fill(friendlyName);
await page.getByTestId("url-input").fill("https://httpbin.org/html");

// Check that keyword field is visible for real-browser monitors
const keywordInput = page.locator("#keyword");
await expect(keywordInput).toBeVisible();

// Check that invert keyword checkbox is visible
const invertKeywordCheckbox = page.locator("#invert-keyword");
await expect(invertKeywordCheckbox).toBeVisible();

// Fill in keyword (should be optional)
await keywordInput.fill("Herman Melville");

await screenshot(testInfo, page);

// Save the monitor
await page.getByTestId("save-button").click();
await page.waitForURL("/dashboard/*");

await screenshot(testInfo, page);
});

test("real-browser monitor with invert keyword", async ({ page }, testInfo) => {
await page.goto("./add");
await login(page);

// Select real-browser monitor type
await selectMonitorType(page, "real-browser");

const friendlyName = "Test Real Browser with Invert Keyword";
await page.getByTestId("friendly-name-input").fill(friendlyName);
await page.getByTestId("url-input").fill("https://httpbin.org/html");

// Fill in keyword that should NOT be found
const keywordInput = page.locator("#keyword");
await keywordInput.fill("NonExistentText123");

// Check invert keyword checkbox
const invertKeywordCheckbox = page.locator("#invert-keyword");
await invertKeywordCheckbox.check();

await screenshot(testInfo, page);

// Save the monitor
await page.getByTestId("save-button").click();
await page.waitForURL("/dashboard/*");

await screenshot(testInfo, page);
});

test("real-browser monitor without keyword (backward compatibility)", async ({ page }, testInfo) => {
await page.goto("./add");
await login(page);

// Select real-browser monitor type
await selectMonitorType(page, "real-browser");

const friendlyName = "Test Real Browser No Keyword";
await page.getByTestId("friendly-name-input").fill(friendlyName);
await page.getByTestId("url-input").fill("https://httpbin.org/html");

// Don't fill keyword field - should still work
const keywordInput = page.locator("#keyword");
await expect(keywordInput).toBeVisible();

await screenshot(testInfo, page);

// Save the monitor - should work without keyword
await page.getByTestId("save-button").click();
await page.waitForURL("/dashboard/*");

await screenshot(testInfo, page);
});
});
Loading