sadpandajoe commented on code in PR #45649:
URL: https://github.com/apache/superset/pull/45649#discussion_r4405614739
##########
superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx:
##########
@@ -425,3 +432,301 @@ test('user with can_duplicate sees duplicate button only
for virtual datasets',
const virtualDuplicateButton = within(virtualRow!).getByTestId('copy');
expect(virtualDuplicateButton).toBeInTheDocument();
});
+
+describe('semantic layer setup transition', () => {
+ const configure = (canWriteLayer: boolean) => {
+ jest
+ .mocked(isFeatureEnabled)
+ .mockImplementation(flag => flag === 'SEMANTIC_LAYERS');
+ setupApiPermissions(['can_read', 'can_write']);
+ fetchMock.get(
+ 'glob:*/api/v1/semantic_layer/',
+ { result: [] },
+ { name: 'setup-layers' },
+ );
+ fetchMock.get(
+ 'glob:*/api/v1/semantic_layer/types',
+ { result: [] },
+ { name: 'setup-types' },
+ );
+ return {
+ ...mockAdminUser,
+ roles: {
+ Custom: [
+ ['can_read', 'SemanticLayer'],
+ ...(canWriteLayer ? [['can_write', 'SemanticLayer']] : []),
+ ],
+ },
+ };
+ };
+
+ const openView = async () => {
+ await userEvent.click(await screen.findByTestId('btn-create-new'));
+ await userEvent.click(
+ await screen.findByText('Semantic View', {
+ selector: '.ant-dropdown-menu-title-content',
+ }),
+ );
+ };
+
+ test('layer writers can switch dialogs and cancel back to a fresh layer
list', async () => {
+ renderDatasetList(configure(true));
+ await openView();
+ await userEvent.click(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ );
+ expect(
+ await screen.findByText(
+ 'No semantic layer types are available. Ask your administrator to
check the semantic layer setup for this deployment.',
+ ),
+ ).toBeInTheDocument();
+ expect(screen.getAllByRole('dialog')).toHaveLength(1);
+ fireEvent.keyDown(screen.getByRole('dialog'), {
+ key: 'Escape',
+ keyCode: 27,
+ });
+ expect(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ ).toBeInTheDocument();
+ expect(screen.getAllByRole('dialog')).toHaveLength(1);
+ expect(fetchMock.callHistory.calls('setup-layers')).toHaveLength(2);
+ expect(
+ fetchMock.callHistory
+ .calls()
+ .filter(call => call.options.method === 'POST'),
+ ).toHaveLength(0);
+ await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
+ await waitFor(() =>
+ expect(screen.getByTestId('btn-create-new')).toHaveFocus(),
+ );
+ });
+
+ test('dataset writers without layer-write permission get administrator
guidance', async () => {
+ renderDatasetList(configure(false));
+ await openView();
+ expect(
+ await screen.findByText(
+ /Ask your administrator to set up a semantic layer or grant you
access/,
+ ),
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Create semantic layer' }),
+ ).not.toBeInTheDocument();
+ });
+
+ test.each([true, false])(
+ 'creation success=%s returns only after a successful save',
+ async succeeds => {
+ const user = configure(true);
+ fetchMock.removeRoutes({ names: ['setup-types'] });
+ fetchMock.get('glob:*/api/v1/semantic_layer/types', {
+ result: [{ id: 'example', name: 'Example provider' }],
+ });
+ fetchMock.post('glob:*/api/v1/semantic_layer/schema/configuration', {
+ result: { type: 'object', properties: {} },
+ });
+ fetchMock.post(
+ 'glob:*/api/v1/semantic_layer/',
+ succeeds ? { result: { uuid: 'new-layer' } } : 500,
+ { name: 'create-layer' },
+ );
+ renderDatasetList(user);
+ await openView();
+ await userEvent.click(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ );
+ const typePicker = await screen.findByRole('combobox', {
+ name: 'Semantic layer type',
+ });
+ await waitFor(() => expect(typePicker).toBeEnabled());
+ fireEvent.mouseDown(typePicker);
+ await userEvent.click(
+ await screen.findByText('Example provider', {
+ selector: '.ant-select-item-option-content',
+ }),
+ );
+ await userEvent.click(screen.getByRole('button', { name: 'Add' }));
+ const create = await screen.findByRole('button', { name: 'Create' });
+ await waitFor(() => expect(create).toBeEnabled());
+ fetchMock.removeRoutes({ names: ['setup-layers'] });
+ fetchMock.get(
+ 'glob:*/api/v1/semantic_layer/',
+ { result: [{ uuid: 'new-layer', name: 'Example provider' }] },
+ { name: 'refreshed-layers' },
+ );
+ await userEvent.click(create);
+ await waitFor(() =>
+ expect(fetchMock.callHistory.calls('create-layer')).toHaveLength(1),
+ );
+ if (succeeds) {
+ expect(
+ await screen.findByRole('combobox', { name: 'Semantic layer' }),
+ ).toHaveValue('');
Review Comment:
`toHaveValue('')` reads the AntD Select's search input, which stays empty
even when an option is selected, and the following `Add` disabled check also
holds while no views are loaded. If the refreshed list auto-selected the new
layer, this test would still pass. Could it assert the placeholder is still
shown, no selected label is rendered, and no runtime/views request has fired
before an explicit selection?
##########
superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx:
##########
@@ -425,3 +432,301 @@ test('user with can_duplicate sees duplicate button only
for virtual datasets',
const virtualDuplicateButton = within(virtualRow!).getByTestId('copy');
expect(virtualDuplicateButton).toBeInTheDocument();
});
+
+describe('semantic layer setup transition', () => {
+ const configure = (canWriteLayer: boolean) => {
+ jest
+ .mocked(isFeatureEnabled)
+ .mockImplementation(flag => flag === 'SEMANTIC_LAYERS');
+ setupApiPermissions(['can_read', 'can_write']);
+ fetchMock.get(
+ 'glob:*/api/v1/semantic_layer/',
+ { result: [] },
+ { name: 'setup-layers' },
+ );
+ fetchMock.get(
+ 'glob:*/api/v1/semantic_layer/types',
+ { result: [] },
+ { name: 'setup-types' },
+ );
+ return {
+ ...mockAdminUser,
+ roles: {
+ Custom: [
+ ['can_read', 'SemanticLayer'],
+ ...(canWriteLayer ? [['can_write', 'SemanticLayer']] : []),
+ ],
+ },
+ };
+ };
+
+ const openView = async () => {
+ await userEvent.click(await screen.findByTestId('btn-create-new'));
+ await userEvent.click(
+ await screen.findByText('Semantic View', {
+ selector: '.ant-dropdown-menu-title-content',
+ }),
+ );
+ };
+
+ test('layer writers can switch dialogs and cancel back to a fresh layer
list', async () => {
+ renderDatasetList(configure(true));
+ await openView();
+ await userEvent.click(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ );
+ expect(
+ await screen.findByText(
+ 'No semantic layer types are available. Ask your administrator to
check the semantic layer setup for this deployment.',
+ ),
+ ).toBeInTheDocument();
+ expect(screen.getAllByRole('dialog')).toHaveLength(1);
+ fireEvent.keyDown(screen.getByRole('dialog'), {
+ key: 'Escape',
+ keyCode: 27,
+ });
+ expect(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ ).toBeInTheDocument();
+ expect(screen.getAllByRole('dialog')).toHaveLength(1);
+ expect(fetchMock.callHistory.calls('setup-layers')).toHaveLength(2);
+ expect(
+ fetchMock.callHistory
+ .calls()
+ .filter(call => call.options.method === 'POST'),
+ ).toHaveLength(0);
+ await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
+ await waitFor(() =>
+ expect(screen.getByTestId('btn-create-new')).toHaveFocus(),
+ );
+ });
+
+ test('dataset writers without layer-write permission get administrator
guidance', async () => {
+ renderDatasetList(configure(false));
+ await openView();
+ expect(
+ await screen.findByText(
+ /Ask your administrator to set up a semantic layer or grant you
access/,
+ ),
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Create semantic layer' }),
+ ).not.toBeInTheDocument();
+ });
+
+ test.each([true, false])(
+ 'creation success=%s returns only after a successful save',
+ async succeeds => {
+ const user = configure(true);
+ fetchMock.removeRoutes({ names: ['setup-types'] });
+ fetchMock.get('glob:*/api/v1/semantic_layer/types', {
+ result: [{ id: 'example', name: 'Example provider' }],
+ });
+ fetchMock.post('glob:*/api/v1/semantic_layer/schema/configuration', {
+ result: { type: 'object', properties: {} },
+ });
+ fetchMock.post(
+ 'glob:*/api/v1/semantic_layer/',
+ succeeds ? { result: { uuid: 'new-layer' } } : 500,
+ { name: 'create-layer' },
+ );
+ renderDatasetList(user);
+ await openView();
+ await userEvent.click(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ );
+ const typePicker = await screen.findByRole('combobox', {
+ name: 'Semantic layer type',
+ });
+ await waitFor(() => expect(typePicker).toBeEnabled());
+ fireEvent.mouseDown(typePicker);
+ await userEvent.click(
+ await screen.findByText('Example provider', {
+ selector: '.ant-select-item-option-content',
+ }),
+ );
+ await userEvent.click(screen.getByRole('button', { name: 'Add' }));
+ const create = await screen.findByRole('button', { name: 'Create' });
+ await waitFor(() => expect(create).toBeEnabled());
+ fetchMock.removeRoutes({ names: ['setup-layers'] });
+ fetchMock.get(
+ 'glob:*/api/v1/semantic_layer/',
+ { result: [{ uuid: 'new-layer', name: 'Example provider' }] },
+ { name: 'refreshed-layers' },
+ );
+ await userEvent.click(create);
+ await waitFor(() =>
+ expect(fetchMock.callHistory.calls('create-layer')).toHaveLength(1),
+ );
+ if (succeeds) {
+ expect(
+ await screen.findByRole('combobox', { name: 'Semantic layer' }),
+ ).toHaveValue('');
+ await waitFor(() =>
+ expect(fetchMock.callHistory.calls('refreshed-layers')).toHaveLength(
+ 1,
+ ),
+ );
+ expect(screen.getByRole('button', { name: 'Add' })).toBeDisabled();
+ } else {
+ await waitFor(() => expect(create).toBeEnabled());
+ expect(
+ screen.queryByRole('combobox', { name: 'Semantic layer' }),
+ ).not.toBeInTheDocument();
+
expect(fetchMock.callHistory.calls('refreshed-layers')).toHaveLength(0);
+ }
+ expect(screen.getAllByRole('dialog')).toHaveLength(1);
+ expect(
+ fetchMock.callHistory
+ .calls()
+ .filter(
+ call =>
+ String(call.url).includes('/semantic_view/') &&
+ call.options.method === 'POST',
+ ),
+ ).toHaveLength(0);
+ },
+ );
+
+ test('late layer creation cannot replace a newer setup session', async () =>
{
+ const user = configure(true);
+ fetchMock.removeRoutes({ names: ['setup-types'] });
+ fetchMock.get('glob:*/api/v1/semantic_layer/types', {
+ result: [{ id: 'example', name: 'Example provider' }],
+ });
+ fetchMock.post('glob:*/api/v1/semantic_layer/schema/configuration', {
+ result: { type: 'object', properties: {} },
+ });
+ let finishSave!: (value: { result: { uuid: string } }) => void;
+ fetchMock.post(
+ 'glob:*/api/v1/semantic_layer/',
+ () =>
+ new Promise(resolve => {
+ finishSave = resolve;
+ }),
+ { name: 'pending-create' },
+ );
+ renderDatasetList(user);
+ await openView();
+ await userEvent.click(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ );
+ const typePicker = await screen.findByRole('combobox', {
+ name: 'Semantic layer type',
+ });
+ await waitFor(() => expect(typePicker).toBeEnabled());
+ fireEvent.mouseDown(typePicker);
+ await userEvent.click(
+ await screen.findByText('Example provider', {
+ selector: '.ant-select-item-option-content',
+ }),
+ );
+ await userEvent.click(screen.getByRole('button', { name: 'Add' }));
+ const create = await screen.findByRole('button', { name: 'Create' });
+ await waitFor(() => expect(create).toBeEnabled());
+ await userEvent.click(create);
+ await waitFor(() =>
+ expect(fetchMock.callHistory.calls('pending-create')).toHaveLength(1),
+ );
+ await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
+ await userEvent.click(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ );
+ expect(
+ await screen.findByRole('combobox', { name: 'Semantic layer type' }),
+ ).toBeInTheDocument();
+ await act(async () => {
+ finishSave({ result: { uuid: 'old-layer' } });
+ await fetchMock.callHistory.flush(true);
+ });
+ expect(
+ screen.getByRole('combobox', { name: 'Semantic layer type' }),
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole('combobox', { name: 'Semantic layer' }),
+ ).not.toBeInTheDocument();
+ expect(fetchMock.callHistory.calls('setup-layers')).toHaveLength(2);
+ });
+
+ test('late layer creation refreshes the view picker after cancel without
stale UI', async () => {
+ const user = configure(true);
+ const addSuccessToast = jest.fn();
+ fetchMock.removeRoutes({ names: ['setup-types'] });
+ fetchMock.get('glob:*/api/v1/semantic_layer/types', {
+ result: [{ id: 'example', name: 'Example provider' }],
+ });
+ fetchMock.post('glob:*/api/v1/semantic_layer/schema/configuration', {
+ result: { type: 'object', properties: {} },
+ });
+ let finishSave!: (value: { result: { uuid: string } }) => void;
+ fetchMock.post(
+ 'glob:*/api/v1/semantic_layer/',
+ () =>
+ new Promise(resolve => {
+ finishSave = resolve;
+ }),
+ { name: 'pending-create' },
+ );
+ renderDatasetList(user, { addSuccessToast });
+ await openView();
+ await userEvent.click(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ );
+ const typePicker = await screen.findByRole('combobox', {
+ name: 'Semantic layer type',
+ });
+ await waitFor(() => expect(typePicker).toBeEnabled());
+ fireEvent.mouseDown(typePicker);
+ await userEvent.click(
+ await screen.findByText('Example provider', {
+ selector: '.ant-select-item-option-content',
+ }),
+ );
+ await userEvent.click(screen.getByRole('button', { name: 'Add' }));
+ const create = await screen.findByRole('button', { name: 'Create' });
+ await waitFor(() => expect(create).toBeEnabled());
+ await userEvent.click(create);
+ await waitFor(() =>
+ expect(fetchMock.callHistory.calls('pending-create')).toHaveLength(1),
+ );
+ await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
+ expect(
+ await screen.findByText(/No semantic layers are available to you/),
+ ).toBeInTheDocument();
+ expect(fetchMock.callHistory.calls('setup-layers')).toHaveLength(2);
+ fetchMock.removeRoutes({ names: ['setup-layers'] });
+ fetchMock.get(
+ 'glob:*/api/v1/semantic_layer/',
+ { result: [{ uuid: 'new-layer', name: 'Example provider' }] },
+ { name: 'refreshed-layers' },
+ );
+ await act(async () => {
+ finishSave({ result: { uuid: 'new-layer' } });
+ await fetchMock.callHistory.flush(true);
+ });
+ await waitFor(() =>
+ expect(fetchMock.callHistory.calls('refreshed-layers')).toHaveLength(1),
+ );
+ const layerPicker = screen.getByRole('combobox', {
+ name: 'Semantic layer',
+ });
+ expect(layerPicker).toBeEnabled();
+ fireEvent.mouseDown(layerPicker);
+ expect(
+ await screen.findByText('Example provider', {
+ selector: '.ant-select-item-option-content',
+ }),
+ ).toBeInTheDocument();
+ expect(screen.getAllByRole('dialog')).toHaveLength(1);
+ expect(addSuccessToast).not.toHaveBeenCalled();
+ });
+
+ test('flag off keeps the setup entry unavailable', async () => {
+ const user = configure(true);
+ jest.mocked(isFeatureEnabled).mockReturnValue(false);
+ renderDatasetList(user);
+ expect(await screen.findByText('Datasets')).toBeInTheDocument();
+ expect(screen.queryByTestId('btn-create-new')).not.toBeInTheDocument();
Review Comment:
This assertion passes before the permission response arrives: `Datasets`
renders immediately, and `canCreate` is false until `_info` resolves regardless
of the feature flag. If the flag guard were dropped, the semantic setup entry
could still appear after load and this test would stay green. Could this wait
for the ordinary dataset creation entry (or the resolved `_info` call) before
asserting the semantic entry is absent?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]