Compare commits

...

5 Commits

Author SHA1 Message Date
Claude Code
4ca1d33896 fix(handlebars): register a dayjs-backed formatDate helper (#32960)
The `formatDate` Handlebars helper provided by `just-handlebars-helpers`
resolves moment lazily via `global.moment` /
`require('moment/min/moment-with-locales')`. After the viewer switched from
moment to dayjs, that lookup is never satisfied in the production bundle, so
`{{formatDate 'DD.MM.YYYY' ds}}` rendered "i is not a function" (minified) /
"moment is not a function" (dev) instead of a date.

Register a dayjs-backed `formatDate` override after the library helpers,
preserving the `{{formatDate formatString date [locale]}}` signature, so
existing templates keep working without pulling moment into the bundle.

The accompanying Playwright spec guards the fix; the failure is a bundling
artifact that does not reproduce under Jest's Node `require`, so an E2E test
is required.

Closes #32960

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:49:33 -07:00
Evan
003f292dec test(handlebars): use apiPostChart, ExplorePage object, and TIMEOUT constants
Address reviewer nits: replace raw apiPost with apiPostChart helper, use
the ExplorePage page object (new goto helper) with waitForPageLoad, parse
both { id } and { result: { id } } chart-creation response shapes, and
swap hardcoded millisecond timeouts for TIMEOUT constants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 14:19:11 -07:00
Evan
5be8142f7d test(handlebars): reuse getDatasetByName helper and bump test timeout
Reuse the existing rison-based getDatasetByName helper instead of
duplicating dataset lookup logic, raise the per-test timeout to
TIMEOUT.SLOW_TEST since the test chains several slow steps, and trim the
verbose rationale comment per review feedback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 12:25:43 -07:00
Evan
eb2ba20066 test(handlebars): type page param as Page instead of any
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:58:09 -07:00
Claude Code
51555ab471 test(handlebars): E2E validation that formatDate helper renders a date (#32960)
Regression test for #32960: the `formatDate` Handlebars helper started
rendering "i is not a function" (minified) / "moment is not a function" (dev)
after 4.1.2. The helper (from just-handlebars-helpers) resolves moment lazily,
which the bundled HandlebarsViewer no longer satisfies after switching to dayjs.

This Playwright spec creates a Handlebars chart using
`{{formatDate 'DD.MM.YYYY' ds}}` and asserts it renders a real formatted date
instead of the helper error. It fails red on master, faithfully reproducing
the bug, and turns green once the helper works again.

Closes #32960

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 16:48:09 -07:00
3 changed files with 139 additions and 0 deletions

View File

@@ -29,12 +29,33 @@ export class ExplorePage {
private static readonly SELECTORS = {
DATASOURCE_CONTROL: '[data-test="datasource-control"]',
VIZ_SWITCHER: '[data-test="fast-viz-switcher"]',
CHART_CONTAINER: '[data-test="chart-container"]',
} as const;
constructor(page: Page) {
this.page = page;
}
/**
* Navigates to the Explore page for a given chart and waits for it to load.
*
* @param chartId - ID of the chart (slice) to open
* @param options - Optional wait options
*/
async goto(chartId: number, options?: { timeout?: number }): Promise<void> {
await this.page.goto(`explore/?slice_id=${chartId}`);
await this.waitForPageLoad(options);
}
/**
* Gets the chart container locator (where the rendered viz appears).
*
* @returns Locator for the chart container
*/
getChartContainer(): Locator {
return this.page.locator(ExplorePage.SELECTORS.CHART_CONTAINER);
}
/**
* Waits for the Explore page to load.
* Validates URL contains /explore/ and datasource control is visible.

View File

@@ -0,0 +1,102 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Regression for #32960: the `formatDate` Handlebars helper (provided by
* just-handlebars-helpers) stopped working after 4.1.2, rendering
* "i is not a function" (minified) / "moment is not a function" (dev) instead
* of the formatted date. The library helper resolves `moment` lazily via
* `global.moment` / `require('moment/min/moment-with-locales')`, which the
* bundled HandlebarsViewer no longer satisfies (it switched to dayjs).
*
* The fix registers a dayjs-backed `formatDate` override in HandlebarsViewer
* (superset-frontend/plugins/plugin-chart-handlebars). This spec guards it: it
* creates a Handlebars chart whose template uses `{{formatDate 'DD.MM.YYYY' ds}}`
* and asserts the chart renders a real formatted date rather than the helper
* error. Because the failure was a bundling/minification artifact (moment
* resolves fine under Jest's Node `require`), an E2E test is required to cover it.
*/
import { testWithAssets, expect } from '../../helpers/fixtures';
import { apiPostChart } from '../../helpers/api/chart';
import { getDatasetByName } from '../../helpers/api/dataset';
import { ExplorePage } from '../../pages/ExplorePage';
import { TIMEOUT } from '../../utils/constants';
const DATASET_NAME = 'birth_names';
testWithAssets(
'Handlebars formatDate helper renders a formatted date (#32960)',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
const dataset = await getDatasetByName(page, DATASET_NAME);
if (!dataset) {
throw new Error(`Dataset ${DATASET_NAME} not found`);
}
const datasetId = dataset.id;
const params = {
datasource: `${datasetId}__table`,
viz_type: 'handlebars',
query_mode: 'aggregate',
groupby: ['ds'],
metrics: ['count'],
adhoc_filters: [],
row_limit: 5,
handlebarsTemplate:
"<ul class='data-list'>{{#each data}}" +
"<li class='hb-item'>{{formatDate 'DD.MM.YYYY' ds}}</li>" +
'{{/each}}</ul>',
styleTemplate: '',
};
const chartResp = await apiPostChart(page, {
slice_name: `handlebars_format_date_${Date.now()}`,
viz_type: 'handlebars',
datasource_id: datasetId,
datasource_type: 'table',
params: JSON.stringify(params),
});
expect(chartResp.ok()).toBe(true);
// The chart API may return either a top-level `{ id }` or a wrapped
// `{ result: { id } }` shape; handle both and fail explicitly otherwise.
const chartBody = await chartResp.json();
const chartId: number = chartBody.result?.id ?? chartBody.id;
expect(chartId, 'chart creation should return an id').toBeTruthy();
testAssets.trackChart(chartId);
const explorePage = new ExplorePage(page);
await explorePage.goto(chartId);
const panel = explorePage.getChartContainer();
await panel.waitFor({ state: 'visible', timeout: TIMEOUT.PAGE_LOAD });
// The helper error surfaces as a "... is not a function" message rendered
// in place of the chart content.
await expect(panel).not.toContainText('is not a function', {
timeout: TIMEOUT.API_RESPONSE,
});
// At least one list item should contain a DD.MM.YYYY formatted date.
await expect(panel.locator('li.hb-item').first()).toHaveText(
/\d{2}\.\d{2}\.\d{4}/,
{ timeout: TIMEOUT.API_RESPONSE },
);
},
);

View File

@@ -116,3 +116,19 @@ Handlebars.registerHelper('parseJson', (jsonString: string) => {
Helpers.registerHelpers(Handlebars);
HandlebarsGroupBy.register(Handlebars);
// `just-handlebars-helpers` registers a `formatDate` helper that lazily
// resolves `moment` via `global.moment` / `require('moment/min/moment-with-locales')`.
// The bundled viewer switched to dayjs and never satisfies that lookup, so the
// original helper throws "... is not a function" (see #32960). Re-register a
// dayjs-backed `formatDate` with the same `{{formatDate formatString date [locale]}}`
// signature so existing templates keep rendering.
Handlebars.registerHelper('formatDate', (formatString, date, localeString) => {
const format = typeof formatString === 'string' ? formatString : '';
const instance = dayjs(date || new Date());
// Handlebars always passes its options object as the final argument, so a
// locale is only present when the caller supplied an explicit string.
return typeof localeString === 'string'
? instance.locale(localeString).format(format)
: instance.format(format);
});