mikebridge commented on code in PR #45649:
URL: https://github.com/apache/superset/pull/45649#discussion_r4407242986


##########
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:
   Good point. In `44f812c680` the test holds `_info` pending, waits until 
write permission unlocks the ordinary dataset creation controls, and only then 
checks that the semantic entry is absent. With the flag guard removed, the old 
early assertion still passed under that delay, and the new one fails on the 
unwanted entry.
   



##########
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:
   Tightened in `44f812c680`. After the refreshed list returns, the test checks 
that the picker still shows its placeholder, that no item is selected, and that 
no runtime-schema or views request has fired before an explicit selection. 
Auto-selecting the refreshed layer passed the old empty-search-input check and 
fails the new placeholder check.
   



-- 
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]

Reply via email to