Skip to content

Commit

Permalink
Merge branch 'charting/web-components' of https://github.com/microsof…
Browse files Browse the repository at this point in the history
…t/fluentui into charting/web-components
  • Loading branch information
AtishayMsft committed Nov 14, 2024
2 parents 3049038 + a02289e commit 949ff13
Show file tree
Hide file tree
Showing 5 changed files with 963 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { test } from '@playwright/test';
import { expect, fixtureURL } from '../helpers.tests.js';
import { ChartDataPoint, ChartProps } from './donut-chart.options.js';
import { teamsDarkTheme } from '@fluentui/tokens';

const points: ChartDataPoint[] = [
{
legend: 'first',
data: 20000,
},
{
legend: 'second',
data: 39000,
},
];

const data: ChartProps = {
chartTitle: 'Donut chart basic example',
chartData: points,
};

test.describe('Donut-chart - Basic', () => {
test.beforeEach(async ({ page }) => {
await page.goto(fixtureURL('components-donutchart--basic'));
await page.setContent(/* html */ `
<div>
<fluent-donut-chart value-inside-donut="39,000" inner-radius="55" data='${JSON.stringify(data)}'>
</fluent-donut-chart>
</div>
`);
await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart'));
});

test('Should render chart properly', async ({ page }) => {
const element = page.locator('fluent-donut-chart');
const legends = element.locator('.legend-text');
await expect(legends.nth(0).getByText('first')).toBeVisible();
await expect(legends.nth(1).getByText('second')).toBeVisible();
await expect(element.getByText('39,000')).toBeVisible();
});

test('Should render path with proper attributes and css', async ({ page }) => {
const element = page.locator('fluent-donut-chart');
const arcList = element.locator('.arc');
await expect(arcList).toHaveCount(2);
await expect(arcList.nth(0)).toHaveAttribute('fill', '#637cef');
await expect(arcList.nth(0)).toHaveAttribute('aria-label', 'first, 20000.');
await expect(arcList.nth(0)).toHaveAttribute(
'd',
'M-76.547,47.334A90,90,0,0,1,-1.055,-89.994L-1.055,-54.99A55,55,0,0,0,-46.993,28.577Z',
);
await expect(arcList.nth(0)).toHaveCSS('fill', 'rgb(99, 124, 239)');
await expect(arcList.nth(0)).toHaveCSS('--borderRadiusMedium', '4px');

await expect(arcList.nth(1)).toHaveAttribute('fill', '#e3008c');
await expect(arcList.nth(1)).toHaveAttribute('aria-label', 'second, 39000.');
await expect(arcList.nth(1)).toHaveAttribute(
'd',
'M1.055,-89.994A90,90,0,1,1,-75.417,49.115L-45.863,30.358A55,55,0,1,0,1.055,-54.99Z',
);
await expect(arcList.nth(1)).toHaveCSS('fill', 'rgb(227, 0, 140)');
await expect(arcList.nth(1)).toHaveCSS('--borderRadiusMedium', '4px');
});

test('Should render legends data properly', async ({ page }) => {
const element = page.locator('fluent-donut-chart');
const legends = element.getByRole('option');
await expect(legends).toHaveCount(2);
const firstLegend = element.getByRole('option', { name: 'First' });
const secondLegend = element.getByRole('option', { name: 'Second' });
await expect(firstLegend).toBeVisible();
await expect(firstLegend).toHaveText('first');
await expect(firstLegend).toHaveCSS('--borderRadiusMedium', '4px');
await expect(secondLegend).toBeVisible();
await expect(secondLegend).toHaveText('second');
await expect(secondLegend).toHaveCSS('--borderRadiusMedium', '4px');
});

test('Should update path css values with mouse click event on legend', async ({ page }) => {
const element = page.locator('fluent-donut-chart');
const firstPath = element.getByLabel('first,');
const secondPath = element.getByLabel('second,');
const firstLegend = element.getByRole('option', { name: 'First' });
//mouse events
await firstLegend.click();
await expect(firstPath).toHaveCSS('opacity', '1');
await expect(secondPath).toHaveCSS('opacity', '0.1');
await firstLegend.click();
await expect(firstPath).toHaveCSS('opacity', '1');
await expect(secondPath).toHaveCSS('opacity', '1');
});

test('Should update path css values with mouse hover event on legend', async ({ page }) => {
const element = page.locator('fluent-donut-chart');
const firstPath = element.getByLabel('first,');
const secondPath = element.getByLabel('second,');
const firstLegend = element.getByRole('option', { name: 'First' });
//mouse events
await firstLegend.dispatchEvent('mouseover');
await expect(firstPath).toHaveCSS('opacity', '1');
await expect(secondPath).toHaveCSS('opacity', '0.1');
await firstLegend.dispatchEvent('mouseout');
await expect(firstPath).toHaveCSS('opacity', '1');
await expect(secondPath).toHaveCSS('opacity', '1');
});

test('Should show callout with mouse hover event on path', async ({ page }) => {
const element = page.locator('fluent-donut-chart');
const firstPath = element.getByLabel('first,');
const calloutRoot = element.locator('.tooltip');
await expect(calloutRoot).toHaveCount(0);
await firstPath.dispatchEvent('mouseover');
await expect(calloutRoot).toHaveCount(1);
await expect(calloutRoot).toHaveCSS('opacity', '1');
const calloutLegendText = await element.locator('.tooltip-legend-text');
await expect(calloutLegendText).toHaveText('first');
const calloutContentY = await element.locator('.tooltip-content-y');
await expect(calloutContentY).toHaveText('20000');
await firstPath.dispatchEvent('mouseout');
await expect(calloutRoot).not.toHaveCSS('opacity', '0');
});

test('Should update callout data when mouse moved from one path to another path', async ({ page }) => {
const element = page.locator('fluent-donut-chart');
const firstPath = element.getByLabel('first,');
const calloutRoot = element.locator('.tooltip');
await expect(calloutRoot).toHaveCount(0);
await firstPath.dispatchEvent('mouseover');
await expect(calloutRoot).toHaveCSS('opacity', '1');
const calloutLegendText = await element.locator('.tooltip-legend-text');
await expect(calloutLegendText).toHaveText('first');
const calloutContentY = await element.locator('.tooltip-content-y');
await expect(calloutContentY).toHaveText('20000');
const secondPath = element.getByLabel('second,');
await secondPath.dispatchEvent('mouseover');
await expect(calloutRoot).toHaveCSS('opacity', '1');
await expect(calloutLegendText).toHaveText('second');
await expect(calloutContentY).toHaveText('39000');
});
});
86 changes: 86 additions & 0 deletions packages/charts/chart-web-components/src/helpers.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import qs from 'qs';
import { expect as baseExpect, type ExpectMatcherState, type Locator } from '@playwright/test';

/**
* Returns a formatted URL for a given Storybook fixture.
*
* @param id - the Storybook fixture ID
* @param args - Story args
* @returns - the local URL for the Storybook fixture iframe
*/
export function fixtureURL(id: string = 'debug--blank', args?: Record<string, any>): string {
const params: Record<string, any> = { id };
if (args) {
params.args = qs
.stringify(args, {
allowDots: true,
delimiter: ';',
format: 'RFC1738',
encode: false,
})
.replace(/=/g, ':')
.replace(/\//g, '--');
}

const url = qs.stringify(params, {
addQueryPrefix: true,
format: 'RFC1738',
encode: false,
});

return url;
}

/**
* Evaluate whether an element has the given state or not on its `elementInternals` property.
*
* @param locator - The Playwright locator for the element.
* @param state - The name of the state.
* @param expected - Whether the given state is expected to exist.
* @param has - Whether the element is expected to have or not have the given state, defaults to `true`.
*/
async function toHaveCustomState(
this: ExpectMatcherState,
locator: Locator,
state: string,
options?: { timeout?: number },
) {
const assertionName = 'toHaveCustomState';
let pass: boolean;
let matcherResult: any;
const expected: boolean = !this.isNot;

try {
baseExpect(await locator.evaluate((el, state) => el.matches(`:state(${state})`), state, options)).toEqual(true);
pass = true;
} catch (err: any) {
matcherResult = err.matcherResult;
pass = false;
}

const message = pass
? () =>
this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
'\n\n' +
`Locator: ${locator}\n` +
`Expected: ${this.isNot ? 'not' : ''}${this.utils.printExpected(expected)}\n` +
(matcherResult ? `Received: ${this.utils.printReceived(matcherResult.actual)}` : '')
: () =>
this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
'\n\n' +
`Locator: ${locator}\n` +
`Expected: ${this.utils.printExpected(expected)}\n` +
(matcherResult ? `Received: ${this.utils.printReceived(matcherResult.actual)}` : '');

return {
name: assertionName,
message,
pass,
expected,
actual: matcherResult?.actual,
};
}

export const expect = baseExpect.extend({
toHaveCustomState,
});
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const styles: ElementStyles = css`
stroke-width: 2px;
stroke: black;
}
.svgChart {
.svg-chart {
display: block;
overflow: visible;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ export class HorizontalBarChart extends FASTElement {
.append('svg')
.attr('height', 12)
.attr('width', 100 + '%')
.attr('class', 'svgChart')
.attr('class', 'svg-chart')
.attr('aria-label', data?.chartTitle ? data?.chartTitle : '')
.selectAll('g')
.data(data.chartData!)
Expand Down Expand Up @@ -372,7 +372,7 @@ export class HorizontalBarChart extends FASTElement {
.append('text')
.attr('key', 'text')
.attr('style', 'margin-top: -4.5px; margin-left: 2px;')
.attr('class', 'barLabel')
.attr('class', 'bar-label')
.attr(
'x',
`${
Expand All @@ -392,7 +392,7 @@ export class HorizontalBarChart extends FASTElement {
svgEle
.append('text')
.attr('key', 'text')
.attr('class', 'barLabel')
.attr('class', 'bar-label')
.attr(
'x',
`${
Expand Down
Loading

0 comments on commit 949ff13

Please sign in to comment.