Skip to content
Merged
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
5 changes: 5 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

272 changes: 187 additions & 85 deletions e2e/screenshots.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,42 @@ const MOCK_DASHBOARD_RESPONSE = {
},
status: { componentDataStatus: 'DATA', refreshStatus: 'IDLE' },
},
{
componentId: 'comp-metric',
reportResult: {
factMap: {
'T!T': { aggregates: [{ label: '42', value: 42 }] },
},
groupingsDown: { groupings: [] },
groupingsAcross: { groupings: [] },
reportExtendedMetadata: {
aggregateColumnInfo: {
RowCount: { dataType: 'int', label: 'Record Count' },
},
detailColumnInfo: {},
},
reportMetadata: { aggregates: ['RowCount'], detailColumns: [] },
},
status: { componentDataStatus: 'DATA', refreshStatus: 'IDLE' },
},
{
componentId: 'comp-gauge',
reportResult: {
factMap: {
'T!T': { aggregates: [{ label: '135', value: 135 }] },
},
groupingsDown: { groupings: [] },
groupingsAcross: { groupings: [] },
reportExtendedMetadata: {
aggregateColumnInfo: {
RowCount: { dataType: 'int', label: 'Record Count' },
},
detailColumnInfo: {},
},
reportMetadata: { aggregates: ['RowCount'], detailColumns: [] },
},
status: { componentDataStatus: 'DATA', refreshStatus: 'IDLE' },
},
{
componentId: 'comp-table',
reportResult: {
Expand Down Expand Up @@ -176,8 +212,50 @@ const MOCK_DASHBOARD_RESPONSE = {
},
},
{
id: 'comp-table',
id: 'comp-metric',
componentData: 3,
header: 'Total Cases',
title: null,
reportId: '00O000000000005',
type: 'Report',
properties: {
visualizationType: 'Metric',
visualizationProperties: {},
aggregates: [{ name: 'RowCount' }],
groupings: null,
},
},
{
id: 'comp-gauge',
componentData: 4,
header: 'Number of Cases',
title: null,
reportId: '00O000000000004',
type: 'Report',
properties: {
visualizationType: 'Gauge',
visualizationProperties: {
breakPoints: [
{
aggregateName: 'RowCount',
breaks: [
{ color: 'ae4034', lowerBound: 0, upperBound: 54 },
{ color: 'f2a91e', lowerBound: 54, upperBound: 108 },
{ color: '217c3e', lowerBound: 108, upperBound: 200 },
],
},
],
showPercentages: false,
showRange: true,
showTotal: false,
},
aggregates: [{ name: 'RowCount' }],
groupings: null,
},
},
{
id: 'comp-table',
componentData: 5,
header: 'Account List',
title: null,
reportId: '00O000000000002',
Expand All @@ -202,7 +280,9 @@ const MOCK_DASHBOARD_RESPONSE = {
{ column: 0, row: 0, colspan: 4, rowspan: 8 },
{ column: 4, row: 0, colspan: 4, rowspan: 8 },
{ column: 8, row: 0, colspan: 4, rowspan: 8 },
{ column: 0, row: 8, colspan: 12, rowspan: 6 },
{ column: 0, row: 8, colspan: 3, rowspan: 8 },
{ column: 3, row: 8, colspan: 5, rowspan: 8 },
{ column: 8, row: 8, colspan: 4, rowspan: 8 },
],
numColumns: 12,
rowHeight: 36,
Expand Down Expand Up @@ -311,18 +391,41 @@ const { screenlyJsContent: reportScreenlyJsContent } =
}
)

const { screenlyJsContent: dashboardLabelsScreenlyJsContent } =
createMockScreenlyForScreenshots(
{ coordinates: [37.3861, -122.0839], location: 'Silicon Valley, USA' },
{
content_id: MOCK_DASHBOARD_ID,
refresh_interval: '300',
display_errors: 'false',
show_labels: 'true',
screenly_oauth_tokens_url: 'http://localhost:3000/',
screenly_app_auth_token: 'mock-token',
}
)

const { screenlyJsContent: reportLabelsScreenlyJsContent } =
createMockScreenlyForScreenshots(
{ coordinates: [37.3861, -122.0839], location: 'Silicon Valley, USA' },
{
content_id: MOCK_REPORT_ID,
refresh_interval: '300',
display_errors: 'false',
show_labels: 'true',
screenly_oauth_tokens_url: 'http://localhost:3000/',
screenly_app_auth_token: 'mock-token',
}
)

type BrowserContext = Awaited<ReturnType<Browser['newContext']>>

async function takeScreenshot(
browser: Browser,
width: number,
height: number,
filename: string,
screenlyJsContent: string,
setup: (context: Awaited<ReturnType<Browser['newContext']>>) => Promise<void>,
waitFor: (
page: Awaited<
ReturnType<Awaited<ReturnType<Browser['newContext']>>['newPage']>
>
) => Promise<void>
setup: (context: BrowserContext) => Promise<void>
): Promise<void> {
const screenshotsDir = getScreenshotsDir()
const context = await browser.newContext({ viewport: { width, height } })
Expand All @@ -333,7 +436,7 @@ async function takeScreenshot(
await setup(context)

await page.goto('/?animations=false')
await waitFor(page)
await page.waitForLoadState('networkidle')

await page.screenshot({
path: path.join(screenshotsDir, filename),
Expand All @@ -343,6 +446,45 @@ async function takeScreenshot(
await context.close()
}

function mockCredentials(context: BrowserContext): Promise<void> {
return context.route(/access_token\//, (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_CREDENTIALS),
})
)
}

async function setupDashboardRoutes(
context: BrowserContext,
dashboardStatus: number
): Promise<void> {
await mockCredentials(context)
await context.route(/analytics\/dashboards/, (route) =>
route.fulfill({
status: dashboardStatus,
contentType: 'application/json',
body: JSON.stringify(
dashboardStatus === 200
? MOCK_DASHBOARD_RESPONSE
: { message: dashboardStatus === 404 ? 'Not Found' : 'Unauthorized' }
),
})
)
}

async function setupReportRoutes(context: BrowserContext): Promise<void> {
await mockCredentials(context)
await context.route(/analytics\/reports/, (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_REPORT_RESPONSE),
})
)
}

for (const { width, height } of RESOLUTIONS) {
test(`screenshot dashboard ${width}x${height}`, async ({ browser }) => {
await takeScreenshot(
Expand All @@ -351,25 +493,7 @@ for (const { width, height } of RESOLUTIONS) {
height,
`dashboard-${width}x${height}.png`,
dashboardScreenlyJsContent,
async (context) => {
await context.route(/access_token\//, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_CREDENTIALS),
})
})
await context.route(/analytics\/dashboards/, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_DASHBOARD_RESPONSE),
})
})
},
async (page) => {
await page.waitForLoadState('networkidle')
}
(context) => setupDashboardRoutes(context, 200)
)
})
}
Expand All @@ -385,25 +509,7 @@ for (const [width, height] of [
height,
`error-${width}x${height}.png`,
dashboardScreenlyJsContent,
async (context) => {
await context.route(/access_token\//, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_CREDENTIALS),
})
})
await context.route(/analytics\/dashboards/, async (route) => {
await route.fulfill({
status: 401,
contentType: 'application/json',
body: JSON.stringify({ message: 'Unauthorized' }),
})
})
},
async (page) => {
await page.waitForLoadState('networkidle')
}
(context) => setupDashboardRoutes(context, 401)
)
})
}
Expand All @@ -415,28 +521,26 @@ test('screenshot error-not-found 3840x2160', async ({ browser }) => {
2160,
'error-not-found-3840x2160.png',
dashboardScreenlyJsContent,
async (context) => {
await context.route(/access_token\//, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_CREDENTIALS),
})
})
await context.route(/analytics\/dashboards/, async (route) => {
await route.fulfill({
status: 404,
contentType: 'application/json',
body: JSON.stringify({ message: 'Not Found' }),
})
})
},
async (page) => {
await page.waitForLoadState('networkidle')
}
(context) => setupDashboardRoutes(context, 404)
)
})

for (const [width, height] of [
[1920, 1080],
[1080, 1920],
]) {
test(`screenshot dashboard-labels ${width}x${height}`, async ({ browser }) => {
await takeScreenshot(
browser,
width,
height,
`dashboard-labels-${width}x${height}.png`,
dashboardLabelsScreenlyJsContent,
(context) => setupDashboardRoutes(context, 200)
)
})
}

for (const [width, height] of [
[3840, 2160],
[2160, 3840],
Expand All @@ -448,25 +552,23 @@ for (const [width, height] of [
height,
`report-${width}x${height}.png`,
reportScreenlyJsContent,
async (context) => {
await context.route(/access_token\//, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_CREDENTIALS),
})
})
await context.route(/analytics\/reports/, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_REPORT_RESPONSE),
})
})
},
async (page) => {
await page.waitForLoadState('networkidle')
}
(context) => setupReportRoutes(context)
)
})
}

for (const [width, height] of [
[1920, 1080],
[1080, 1920],
]) {
test(`screenshot report-labels ${width}x${height}`, async ({ browser }) => {
await takeScreenshot(
browser,
width,
height,
`report-labels-${width}x${height}.png`,
reportLabelsScreenlyJsContent,
(context) => setupReportRoutes(context)
)
})
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,8 @@
"npm-run-all2": "^8.0.4",
"prettier": "^3.8.1",
"typescript": "^5.9.3"
},
"dependencies": {
"chartjs-plugin-datalabels": "^2.2.0"
}
}
10 changes: 10 additions & 0 deletions screenly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,13 @@ settings:
help_text: How often to refresh Salesforce data. Defaults to 300 seconds (5 minutes).
type: number
schema_version: 1
show_labels:
type: string
default_value: 'false'
title: Show Labels
optional: true
help_text:
properties:
help_text: Show data labels on charts and gauges.
type: boolean
schema_version: 1
Loading