Skip to content
Snippets Groups Projects
Unverified Commit 61d40da1 authored by Sameer Al-Sakran's avatar Sameer Al-Sakran Committed by GitHub
Browse files

Merge pull request #6980 from metabase/dash-filter-new-endpoints

FieldValues search new public/embedded endpoints
parents 876fcd48 c5be21fd
No related branches found
No related tags found
No related merge requests found
Showing
with 966 additions and 438 deletions
......@@ -27,8 +27,6 @@ import {
import type { FieldValues } from "metabase/meta/types/Field";
import _ from "underscore";
/**
* Wrapper class for field metadata objects. Belongs to a Table.
*/
......@@ -37,6 +35,7 @@ export default class Field extends Base {
description: string;
table: Table;
name_field: ?Field;
fieldType() {
return getFieldType(this);
......@@ -147,11 +146,8 @@ export default class Field extends Base {
}
// this enables "implicit" remappings from type/PK to type/Name on the same table,
// used in FieldValuesWidget, but not table/object detail listings
if (this.isPK()) {
const nameField = _.find(this.table.fields, f => f.isEntityName());
if (nameField) {
return nameField;
}
if (this.name_field) {
return this.name_field;
}
return null;
}
......
......@@ -159,7 +159,7 @@ export const PublicLinksQuestionListing = () => (
revoke={CardApi.deletePublicLink}
type={t`Public Card Listing`}
getUrl={({ id }) => Urls.question(id)}
getPublicUrl={({ public_uuid }) => Urls.publicCard(public_uuid)}
getPublicUrl={({ public_uuid }) => Urls.publicQuestion(public_uuid)}
noLinksMessage={t`No questions have been publicly shared yet.`}
/>
);
......
......@@ -132,7 +132,7 @@ class BrowserSelect extends Component {
)
}
triggerClasses={className}
verticalAttachments={["top"]}
verticalAttachments={["top", "bottom"]}
isInitiallyOpen={isInitiallyOpen}
{...extraProps}
>
......
......@@ -33,7 +33,11 @@ import Utils from "metabase/lib/utils";
import { getPositionForNewDashCard } from "metabase/lib/dashboard_grid";
import { createCard } from "metabase/lib/card";
import { addParamValues, fetchDatabaseMetadata } from "metabase/redux/metadata";
import {
addParamValues,
addFields,
fetchDatabaseMetadata,
} from "metabase/redux/metadata";
import { push } from "react-router-redux";
import {
......@@ -551,6 +555,9 @@ export const fetchDashboard = createThunkAction(FETCH_DASHBOARD, function(
if (result.param_values) {
dispatch(addParamValues(result.param_values));
}
if (result.param_fields) {
dispatch(addFields(result.param_fields));
}
return {
...normalize(result, dashboard), // includes `result` and `entities`
......
......@@ -6,12 +6,12 @@ import EventEmitter from "events";
type TransformFn = (o: any) => any;
type Options = {
export type Options = {
noEvent?: boolean,
transformResponse?: TransformFn,
cancelled?: Promise<any>,
};
type Data = {
export type Data = {
[key: string]: any,
};
......
......@@ -83,7 +83,7 @@ export function label(label) {
return `/questions/search?label=${encodeURIComponent(label.slug)}`;
}
export function publicCard(uuid, type = null) {
export function publicQuestion(uuid, type = null) {
const siteUrl = MetabaseSettings.get("site_url");
return `${siteUrl}/public/question/${uuid}` + (type ? `.${type}` : ``);
}
......@@ -96,3 +96,7 @@ export function publicDashboard(uuid) {
export function embedCard(token, type = null) {
return `/embed/question/${token}` + (type ? `.${type}` : ``);
}
export function embedDashboard(token) {
return `/embed/dashboard/${token}`;
}
......@@ -26,6 +26,11 @@ import {
import * as dashboardActions from "metabase/dashboard/dashboard";
import {
setPublicDashboardEndpoints,
setEmbedDashboardEndpoints,
} from "metabase/services";
import type { Dashboard } from "metabase/meta/types/Dashboard";
import type { Parameter } from "metabase/meta/types/Parameter";
......@@ -89,6 +94,13 @@ export default class PublicDashboard extends Component {
location,
params: { uuid, token },
} = this.props;
if (uuid) {
setPublicDashboardEndpoints(uuid);
} else if (token) {
setEmbedDashboardEndpoints(token);
}
initialize();
try {
// $FlowFixMe
......
......@@ -20,10 +20,15 @@ import {
applyParameters,
} from "metabase/meta/Card";
import { PublicApi, EmbedApi } from "metabase/services";
import {
PublicApi,
EmbedApi,
setPublicQuestionEndpoints,
setEmbedQuestionEndpoints,
} from "metabase/services";
import { setErrorPage } from "metabase/redux/app";
import { addParamValues } from "metabase/redux/metadata";
import { addParamValues, addFields } from "metabase/redux/metadata";
import { updateIn } from "icepick";
......@@ -34,6 +39,7 @@ type Props = {
height: number,
setErrorPage: (error: { status: number }) => void,
addParamValues: any => void,
addFields: any => void,
};
type State = {
......@@ -45,6 +51,7 @@ type State = {
const mapDispatchToProps = {
setErrorPage,
addParamValues,
addFields,
};
@connect(null, mapDispatchToProps)
......@@ -69,6 +76,13 @@ export default class PublicQuestion extends Component {
params: { uuid, token },
location: { query },
} = this.props;
if (uuid) {
setPublicQuestionEndpoints(uuid);
} else if (token) {
setEmbedQuestionEndpoints(token);
}
try {
let card;
if (token) {
......@@ -82,6 +96,9 @@ export default class PublicQuestion extends Component {
if (card.param_values) {
this.props.addParamValues(card.param_values);
}
if (card.param_fields) {
this.props.addFields(card.param_fields);
}
let parameterValues: ParameterValues = {};
for (let parameter of getParameters(card)) {
......
......@@ -118,7 +118,7 @@ const PublicQueryButton = ({
<DownloadButton
className={className}
method="GET"
url={Urls.publicCard(uuid, type)}
url={Urls.publicQuestion(uuid, type)}
params={{ parameters: JSON.stringify(json_query.parameters) }}
extensions={[type]}
>
......
import React from "react";
import Parameters from "metabase/parameters/components/Parameters";
const MockNativeQueryEditor = ({ location, query, setParameterValue }) => (
<Parameters
parameters={query.question().parameters()}
query={location.query}
setParameterValue={setParameterValue}
syncQueryString
isQB
commitImmediately
/>
);
export default MockNativeQueryEditor;
......@@ -50,7 +50,7 @@ export default class QuestionEmbedWidget extends Component {
updateEmbeddingParams(card, embeddingParams)
}
getPublicUrl={({ public_uuid }, extension) =>
Urls.publicCard(public_uuid, extension)
Urls.publicQuestion(public_uuid, extension)
}
extensions={["csv", "xlsx", "json"]}
/>
......
......@@ -355,7 +355,8 @@ export const fetchField = createThunkAction(FETCH_FIELD, function(
return async function(dispatch, getState) {
const requestStatePath = ["metadata", "fields", fieldId];
const existingStatePath = requestStatePath;
const getData = () => MetabaseApi.field_get({ fieldId });
const getData = async () =>
normalize(await MetabaseApi.field_get({ fieldId }), FieldSchema);
return await fetchData({
dispatch,
......@@ -420,6 +421,11 @@ export const updateFieldValues = createThunkAction(
export const ADD_PARAM_VALUES = "metabase/metadata/ADD_PARAM_VALUES";
export const addParamValues = createAction(ADD_PARAM_VALUES);
export const ADD_FIELDS = "metabase/metadata/ADD_FIELDS";
export const addFields = createAction(ADD_FIELDS, fields => {
return normalize(fields, [FieldSchema]);
});
export const UPDATE_FIELD = "metabase/metadata/UPDATE_FIELD";
export const updateField = createThunkAction(UPDATE_FIELD, function(field) {
return async function(dispatch, getState) {
......@@ -689,15 +695,6 @@ const tables = handleActions({}, {});
const fields = handleActions(
{
[FETCH_FIELD]: {
next: (state, { payload: field }) => ({
...state,
[field.id]: {
...(state[field.id] || {}),
...field,
},
}),
},
[FETCH_FIELD_VALUES]: {
next: (state, { payload: fieldValues }) =>
fieldValues
......
......@@ -22,6 +22,10 @@ TableSchema.define({
FieldSchema.define({
target: FieldSchema,
table: TableSchema,
name_field: FieldSchema,
dimensions: {
human_readable_field: FieldSchema,
},
});
SegmentSchema.define({
......
......@@ -84,13 +84,20 @@ export const getMetadata = createSelector(
hydrateList(meta.tables, "segments", meta.segments);
hydrateList(meta.tables, "metrics", meta.metrics);
hydrate(meta.tables, "db", t => meta.databases[t.db_id || t.db]);
hydrate(meta.segments, "table", s => meta.tables[s.table_id]);
hydrate(meta.metrics, "table", m => meta.tables[m.table_id]);
hydrate(meta.fields, "table", f => meta.tables[f.table_id]);
hydrate(meta.fields, "target", f => meta.fields[f.fk_target_field_id]);
hydrate(meta.tables, "db", t => meta.database(t.db_id || t.db));
hydrate(meta.segments, "table", s => meta.table(s.table_id));
hydrate(meta.metrics, "table", m => meta.table(m.table_id));
hydrate(meta.fields, "table", f => meta.table(f.table_id));
hydrate(meta.fields, "target", f => meta.field(f.fk_target_field_id));
hydrate(meta.fields, "name_field", f => {
if (f.name_field != null) {
return meta.field(f.name_field);
} else if (f.table && f.isPK()) {
return _.find(f.table.fields, f => f.isEntityName());
}
});
hydrate(meta.fields, "operators", f => getOperators(f, f.table));
hydrate(meta.tables, "aggregation_options", t =>
......@@ -221,10 +228,14 @@ export const makeGetMergedParameterFieldValues = () => {
export function copyObjects(metadata, objects, Klass) {
let copies = {};
for (const object of Object.values(objects)) {
// $FlowFixMe
copies[object.id] = new Klass(object);
// $FlowFixMe
copies[object.id].metadata = metadata;
if (object && object.id != null) {
// $FlowFixMe
copies[object.id] = new Klass(object);
// $FlowFixMe
copies[object.id].metadata = metadata;
} else {
console.warn("Missing id:", object);
}
}
return copies;
}
......
......@@ -11,6 +11,8 @@ const embedBase = IS_EMBED_PREVIEW ? "/api/preview_embed" : "/api/embed";
// $FlowFixMe: Flow doesn't understand webpack loader syntax
import getGAMetadata from "promise-loader?global!metabase/lib/ga-metadata"; // eslint-disable-line import/default
import type { Data, Options } from "metabase/lib/api";
export const ActivityApi = {
list: GET("/api/activity"),
recent_views: GET("/api/activity/recent_views"),
......@@ -315,4 +317,41 @@ export const I18NApi = {
locale: GET("/app/locales/:locale.json"),
};
export function setPublicQuestionEndpoints(uuid: string) {
setFieldEndpoints("/api/public/card/:uuid", { uuid });
}
export function setPublicDashboardEndpoints(uuid: string) {
setFieldEndpoints("/api/public/dashboard/:uuid", { uuid });
}
export function setEmbedQuestionEndpoints(token: string) {
if (!IS_EMBED_PREVIEW) {
setFieldEndpoints("/api/embed/card/:token", { token });
}
}
export function setEmbedDashboardEndpoints(token: string) {
if (!IS_EMBED_PREVIEW) {
setFieldEndpoints("/api/embed/dashboard/:token", { token });
}
}
function GET_with(url: string, params: Data) {
return (data: Data, options?: Options) =>
GET(url)({ ...params, ...data }, options);
}
export function setFieldEndpoints(prefix: string, params: Data) {
MetabaseApi.field_values = GET_with(
prefix + "/field/:fieldId/values",
params,
);
MetabaseApi.field_search = GET_with(
prefix + "/field/:fieldId/search/:searchFieldId",
params,
);
MetabaseApi.field_remapping = GET_with(
prefix + "/field/:fieldId/remapping/:remappedFieldId",
params,
);
}
global.services = exports;
......@@ -10,6 +10,7 @@ import "./mocks";
import { format as urlFormat } from "url";
import api from "metabase/lib/api";
import { defer } from "metabase/lib/promise";
import { DashboardApi, SessionApi } from "metabase/services";
import { METABASE_SESSION_COOKIE } from "metabase/lib/cookies";
import normalReducers from "metabase/reducers-main";
......@@ -438,6 +439,17 @@ export const waitForRequestToComplete = (
});
};
export const waitForAllRequestsToComplete = () => {
if (pendingRequests > 0) {
if (!pendingRequestsDeferred) {
pendingRequestsDeferred = defer();
}
return pendingRequestsDeferred.promise;
} else {
return Promise.resolve();
}
};
/**
* Lets you replace given API endpoints with mocked implementations for the lifetime of a test
*/
......@@ -475,68 +487,82 @@ export async function withApiMocks(mocks, test) {
}
}
let pendingRequests = 0;
let pendingRequestsDeferred = null;
// Patches the metabase/lib/api module so that all API queries contain the login credential cookie.
// Needed because we are not in a real web browser environment.
api._makeRequest = async (method, url, headers, requestBody, data, options) => {
const headersWithSessionCookie = {
...headers,
...(loginSession
? { Cookie: `${METABASE_SESSION_COOKIE}=${loginSession.id}` }
: {}),
};
pendingRequests++;
try {
const headersWithSessionCookie = {
...headers,
...(loginSession
? { Cookie: `${METABASE_SESSION_COOKIE}=${loginSession.id}` }
: {}),
};
const fetchOptions = {
credentials: "include",
method,
headers: new Headers(headersWithSessionCookie),
...(requestBody ? { body: requestBody } : {}),
};
const fetchOptions = {
credentials: "include",
method,
headers: new Headers(headersWithSessionCookie),
...(requestBody ? { body: requestBody } : {}),
};
let isCancelled = false;
if (options.cancelled) {
options.cancelled.then(() => {
isCancelled = true;
});
}
const result = simulateOfflineMode
? { status: 0, responseText: "" }
: await fetch(api.basename + url, fetchOptions);
let isCancelled = false;
if (options.cancelled) {
options.cancelled.then(() => {
isCancelled = true;
});
}
const result = simulateOfflineMode
? { status: 0, responseText: "" }
: await fetch(api.basename + url, fetchOptions);
if (isCancelled) {
throw { status: 0, data: "", isCancelled: true };
}
if (isCancelled) {
throw { status: 0, data: "", isCancelled: true };
}
let resultBody = null;
try {
resultBody = await result.text();
// Even if the result conversion to JSON fails, we still return the original text
// This is 1-to-1 with the real _makeRequest implementation
resultBody = JSON.parse(resultBody);
} catch (e) {}
apiRequestCompletedCallback &&
setTimeout(() => apiRequestCompletedCallback(method, url), 0);
if (result.status >= 200 && result.status <= 299) {
if (options.transformResponse) {
return options.transformResponse(resultBody, { data });
let resultBody = null;
try {
resultBody = await result.text();
// Even if the result conversion to JSON fails, we still return the original text
// This is 1-to-1 with the real _makeRequest implementation
resultBody = JSON.parse(resultBody);
} catch (e) {}
apiRequestCompletedCallback &&
setTimeout(() => apiRequestCompletedCallback(method, url), 0);
if (result.status >= 200 && result.status <= 299) {
if (options.transformResponse) {
return options.transformResponse(resultBody, { data });
} else {
return resultBody;
}
} else {
return resultBody;
const error = {
status: result.status,
data: resultBody,
isCancelled: false,
};
if (!simulateOfflineMode) {
console.log(
"A request made in a test failed with the following error:",
);
console.log(error, { depth: null });
console.log(`The original request: ${method} ${url}`);
if (requestBody) console.log(`Original payload: ${requestBody}`);
}
throw error;
}
} else {
const error = {
status: result.status,
data: resultBody,
isCancelled: false,
};
if (!simulateOfflineMode) {
console.log("A request made in a test failed with the following error:");
console.log(error, { depth: null });
console.log(`The original request: ${method} ${url}`);
if (requestBody) console.log(`Original payload: ${requestBody}`);
} finally {
pendingRequests--;
if (pendingRequests === 0 && pendingRequestsDeferred) {
process.nextTick(pendingRequestsDeferred.resolve);
pendingRequestsDeferred = null;
}
throw error;
}
};
......
......@@ -46,8 +46,6 @@ bodyComponent.default = bodyComponent.TestBodyComponent;
import * as table from "metabase/visualizations/visualizations/Table";
table.default = table.TestTable;
jest.mock("metabase/hoc/Remapped");
// Replace addEventListener with a test implementation which collects all event listeners to `eventListeners` map
export let eventListeners = {};
const testAddEventListener = jest.fn((event, listener) => {
......
This diff is collapsed.
import { mount } from "enzyme";
// Converted from an old Selenium E2E test
import {
createDashboard,
createTestStore,
useSharedAdminLogin,
logout,
createTestStore,
restorePreviousLogin,
waitForRequestToComplete,
} from "__support__/integrated_tests";
import { click, clickButton, setInputValue } from "__support__/enzyme_utils";
import { FETCH_DASHBOARD } from "metabase/dashboard/dashboard";
import { mount } from "enzyme";
import { LOAD_CURRENT_USER } from "metabase/redux/user";
import {
INITIALIZE_SETTINGS,
UPDATE_SETTING,
updateSetting,
} from "metabase/admin/settings/settings";
import SettingToggle from "metabase/admin/settings/components/widgets/SettingToggle";
import Toggle from "metabase/components/Toggle";
import EmbeddingLegalese from "metabase/admin/settings/components/widgets/EmbeddingLegalese";
import {
CREATE_PUBLIC_LINK,
INITIALIZE_QB,
API_CREATE_QUESTION,
QUERY_COMPLETED,
RUN_QUERY,
SET_QUERY_MODE,
setDatasetQuery,
UPDATE_EMBEDDING_PARAMS,
UPDATE_ENABLE_EMBEDDING,
UPDATE_TEMPLATE_TAG,
} from "metabase/query_builder/actions";
import NativeQueryEditor from "metabase/query_builder/components/NativeQueryEditor";
import { delay } from "metabase/lib/promise";
import TagEditorSidebar from "metabase/query_builder/components/template_tags/TagEditorSidebar";
import { getQuery } from "metabase/query_builder/selectors";
import {
ADD_PARAM_VALUES,
FETCH_TABLE_METADATA,
} from "metabase/redux/metadata";
import RunButton from "metabase/query_builder/components/RunButton";
import Scalar from "metabase/visualizations/visualizations/Scalar";
import ParameterFieldWidget from "metabase/parameters/components/widgets/ParameterFieldWidget";
import SaveQuestionModal from "metabase/containers/SaveQuestionModal";
import { LOAD_COLLECTIONS } from "metabase/questions/collections";
import SharingPane from "metabase/public/components/widgets/SharingPane";
import { EmbedTitle } from "metabase/public/components/widgets/EmbedModalContent";
import PreviewPane from "metabase/public/components/widgets/PreviewPane";
import CopyWidget from "metabase/components/CopyWidget";
import ListSearchField from "metabase/components/ListSearchField";
import * as Urls from "metabase/lib/urls";
import QuestionEmbedWidget from "metabase/query_builder/containers/QuestionEmbedWidget";
import EmbedWidget from "metabase/public/components/widgets/EmbedWidget";
import { DashboardApi, SettingsApi } from "metabase/services";
async function updateQueryText(store, queryText) {
// We don't have Ace editor so we have to trigger the Redux action manually
const newDatasetQuery = getQuery(store.getState())
.updateQueryText(queryText)
.datasetQuery();
describe("public pages", () => {
beforeAll(async () => {
// needed to create the public dash
useSharedAdminLogin();
});
return store.dispatch(setDatasetQuery(newDatasetQuery));
}
describe("public dashboards", () => {
let dashboard, store, publicDash;
const getRelativeUrlWithoutHash = url =>
url.replace(/#.*$/, "").replace(/http:\/\/.*?\//, "/");
beforeAll(async () => {
store = await createTestStore();
const COUNT_ALL = "200";
const COUNT_DOOHICKEY = "51";
const COUNT_GADGET = "47";
// enable public sharing
await SettingsApi.put({ key: "enable-public-sharing", value: true });
describe("public/embedded", () => {
beforeAll(async () => useSharedAdminLogin());
// create a dashboard
dashboard = await createDashboard({
name: "Test public dash",
description: "A dashboard for testing public things",
});
describe("questions", () => {
let publicUrl = null;
let embedUrl = null;
it("should allow users to enable public sharing", async () => {
const store = await createTestStore();
// load public sharing settings
store.pushPath("/admin/settings/public_sharing");
const app = mount(store.getAppContainer());
await store.waitForActions([LOAD_CURRENT_USER, INITIALIZE_SETTINGS]);
// // if enabled, disable it so we're in a known state
// // TODO Atte Keinänen 8/9/17: This should be done with a direct API call in afterAll instead
const enabledToggleContainer = app.find(SettingToggle).first();
expect(enabledToggleContainer.text()).toBe("Disabled");
// create the public link for that dashboard
publicDash = await DashboardApi.createPublicLink({ id: dashboard.id });
// toggle it on
click(enabledToggleContainer.find(Toggle));
await store.waitForActions([UPDATE_SETTING]);
// make sure it's enabled
expect(enabledToggleContainer.text()).toBe("Enabled");
});
it("should be possible to view a public dashboard", async () => {
store.pushPath(Urls.publicDashboard(publicDash.uuid));
it("should allow users to enable embedding", async () => {
const store = await createTestStore();
// load public sharing settings
store.pushPath("/admin/settings/embedding_in_other_applications");
const app = mount(store.getAppContainer());
await store.waitForActions([FETCH_DASHBOARD]);
await store.waitForActions([LOAD_CURRENT_USER, INITIALIZE_SETTINGS]);
const headerText = app.find(".EmbedFrame-header .h4").text();
click(app.find(EmbeddingLegalese).find('button[children="Enable"]'));
await store.waitForActions([UPDATE_SETTING]);
expect(headerText).toEqual("Test public dash");
expect(app.find(EmbeddingLegalese).length).toBe(0);
const enabledToggleContainer = app.find(SettingToggle).first();
expect(enabledToggleContainer.text()).toBe("Enabled");
});
afterAll(async () => {
// archive the dash so we don't impact other tests
await DashboardApi.update({
id: dashboard.id,
archived: true,
// Note: Test suite is sequential, so individual test cases can't be run individually
it("should allow users to create parameterized SQL questions", async () => {
// Don't render Ace editor in tests because it uses many DOM methods that aren't supported by jsdom
// NOTE Atte Keinänen 8/9/17: Ace provides a MockRenderer class which could be used for pseudo-rendering and
// testing Ace editor in tests, but it doesn't render stuff to DOM so I'm not sure how practical it would be
NativeQueryEditor.prototype.loadAceEditor = () => {};
const store = await createTestStore();
// load public sharing settings
store.pushPath(Urls.plainQuestion());
const app = mount(store.getAppContainer());
await store.waitForActions([INITIALIZE_QB]);
click(app.find(".Icon-sql"));
await store.waitForActions([SET_QUERY_MODE]);
await updateQueryText(
store,
"select count(*) from products where {{category}}",
);
const tagEditorSidebar = app.find(TagEditorSidebar);
const fieldFilterVarType = tagEditorSidebar
.find(".ColumnarSelector-row")
.at(3);
expect(fieldFilterVarType.text()).toBe("Field Filter");
click(fieldFilterVarType);
// there's an async error here for some reason
await store.waitForActions([UPDATE_TEMPLATE_TAG]);
await delay(500);
const productsRow = tagEditorSidebar
.find(".TestPopoverBody .List-section")
.at(4)
.find("a");
expect(productsRow.text()).toBe("Products");
click(productsRow);
// Table fields should be loaded on-the-fly before showing the field selector
await store.waitForActions(FETCH_TABLE_METADATA);
// Needed due to state update after fetching metadata
await delay(100);
const searchField = tagEditorSidebar
.find(".TestPopoverBody")
.find(ListSearchField)
.find("input")
.first();
setInputValue(searchField, "cat");
const categoryRow = tagEditorSidebar
.find(".TestPopoverBody .List-section")
.at(2)
.find("a");
expect(categoryRow.text()).toBe("Category");
click(categoryRow);
await store.waitForActions([UPDATE_TEMPLATE_TAG]);
// close the template variable sidebar
click(tagEditorSidebar.find(".Icon-close"));
// test without the parameter
click(app.find(RunButton));
await store.waitForActions([RUN_QUERY, QUERY_COMPLETED]);
expect(app.find(Scalar).text()).toBe(COUNT_ALL);
// test the parameter
const parameter = app.find(ParameterFieldWidget).first();
click(parameter.find("div").first());
click(parameter.find('span[children="Doohickey"]'));
clickButton(parameter.find(".Button"));
click(app.find(RunButton));
await store.waitForActions([RUN_QUERY, QUERY_COMPLETED]);
expect(app.find(Scalar).text()).toBe(COUNT_DOOHICKEY);
// save the question, required for public link/embedding
click(
app
.find(".Header-buttonSection a")
.first()
.find("a"),
);
await store.waitForActions([LOAD_COLLECTIONS]);
setInputValue(
app.find(SaveQuestionModal).find("input[name='name']"),
"sql parametrized",
);
clickButton(
app
.find(SaveQuestionModal)
.find("button")
.last(),
);
await store.waitForActions([API_CREATE_QUESTION]);
await delay(100);
click(app.find('#QuestionSavedModal .Button[children="Not now"]'));
// wait for modal to close :'(
await delay(500);
// open sharing panel
click(app.find(QuestionEmbedWidget).find(EmbedWidget));
// "Embed this question in an application"
click(
app
.find(SharingPane)
.find("h3")
.last(),
);
// make the parameter editable
click(app.find(".AdminSelect-content[children='Disabled']"));
click(app.find(".TestPopoverBody .Icon-pencil"));
await delay(500);
click(app.find("div[children='Publish']"));
await store.waitForActions([
UPDATE_ENABLE_EMBEDDING,
UPDATE_EMBEDDING_PARAMS,
]);
// save the embed url for next tests
embedUrl = getRelativeUrlWithoutHash(
app
.find(PreviewPane)
.find("iframe")
.prop("src"),
);
// back to main share panel
click(app.find(EmbedTitle));
// toggle public link on
click(app.find(SharingPane).find(Toggle));
await store.waitForActions([CREATE_PUBLIC_LINK]);
// save the public url for next tests
publicUrl = getRelativeUrlWithoutHash(
app
.find(CopyWidget)
.find("input")
.first()
.prop("value"),
);
});
describe("as an anonymous user", () => {
beforeAll(() => logout());
async function runSharedQuestionTests(store, questionUrl, apiRegex) {
store.pushPath(questionUrl);
const app = mount(store.getAppContainer());
await store.waitForActions([ADD_PARAM_VALUES]);
// Loading the query results is done in PublicQuestion itself so we have to listen to API request instead of Redux action
await waitForRequestToComplete("GET", apiRegex);
// use `update()` because of setState
expect(
app
.update()
.find(Scalar)
.text(),
).toBe(COUNT_ALL + "sql parametrized");
// NOTE: parameters tests moved to parameters.integ.spec.js
// set parameter via url
store.pushPath("/"); // simulate a page reload by visiting other page
store.pushPath(questionUrl + "?category=Gadget");
await waitForRequestToComplete("GET", apiRegex);
// use `update()` because of setState
expect(
app
.update()
.find(Scalar)
.text(),
).toBe(COUNT_GADGET + "sql parametrized");
}
it("should allow seeing an embedded question", async () => {
if (!embedUrl)
throw new Error(
"This test fails because previous tests didn't produce an embed url.",
);
const embedUrlTestStore = await createTestStore({ embedApp: true });
await runSharedQuestionTests(
embedUrlTestStore,
embedUrl,
new RegExp("/api/embed/card/.*/query"),
);
});
it("should allow seeing a public question", async () => {
if (!publicUrl)
throw new Error(
"This test fails because previous tests didn't produce a public url.",
);
const publicUrlTestStore = await createTestStore({ publicApp: true });
await runSharedQuestionTests(
publicUrlTestStore,
publicUrl,
new RegExp("/api/public/card/.*/query"),
);
});
// do some cleanup so that we don't impact other tests
await SettingsApi.put({ key: "enable-public-sharing", value: false });
// I think it's cleanest to restore the login here so that there are no surprises if you want to add tests
// that expect that we're already logged in
afterAll(() => restorePreviousLogin());
});
afterAll(async () => {
const store = await createTestStore();
// Disable public sharing and embedding after running tests
await store.dispatch(
updateSetting({ key: "enable-public-sharing", value: false }),
);
await store.dispatch(
updateSetting({ key: "enable-embedding", value: false }),
);
});
});
});
jest.mock("metabase/hoc/Remapped");
// Important: import of integrated_tests always comes first in tests because of mocked modules
import {
createTestStore,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment