diff --git a/src/browser/App.js b/src/browser/App.js index ef4c094..186f51b 100644 --- a/src/browser/App.js +++ b/src/browser/App.js @@ -11,7 +11,7 @@ import SubmitButton from "./component/SubmitButton"; import RelatedListBox from "./component/RelatedListBox"; import ServiceList from "./component/ServiceList"; import AppContext from "./AppContext"; -import serviceManger from "./service-instance"; +import serviceManger, { waitForInitialization } from "./service-instance"; const ipcRenderer = require("electron").ipcRenderer; const appContext = new AppContext(); @@ -26,9 +26,24 @@ class App extends React.Component { }, appContext.ServiceStore.state ); + + // サービスの初期化状態は別管理(ServiceStoreの変更で上書きされないように) + this._serviceInitialized = false; } - componentDidMount() { + async componentDidMount() { + // サービスの初期化を待つ + try { + await waitForInitialization(); + this._serviceInitialized = true; + this.forceUpdate(); + } catch (error) { + console.error("Failed to initialize services:", error); + // エラーが発生しても表示(シークレットがフォールバック値になる) + this._serviceInitialized = true; + this.forceUpdate(); + } + // ServiceStore の変更を監視(componentDidMountで登録) this.unsubscribe = appContext.ServiceStore.onChange(() => { let newState = Object.assign({}, this.state, appContext.ServiceStore.state); @@ -117,6 +132,19 @@ class App extends React.Component { } render() { + if (!this._serviceInitialized) { + return ( +
+
+

Loading...

+
+
+ ); + } + const { ServiceAction } = appContext; const selectTags = ServiceAction.selectTags.bind(ServiceAction); const updateTitle = ServiceAction.updateTitle.bind(ServiceAction); @@ -197,6 +225,8 @@ class App extends React.Component { appContext.on("dispatch", ({ eventKey }) => { ipcRenderer.send(String(eventKey)); }); + +// アプリを即座に起動(初期化はコンポーネント内で処理) try { const root = createRoot(document.getElementById("js-main")); root.render(); diff --git a/src/browser/service-instance.js b/src/browser/service-instance.js index d77a11c..31917d8 100644 --- a/src/browser/service-instance.js +++ b/src/browser/service-instance.js @@ -5,33 +5,75 @@ import ServiceManger from "./service-manager"; // FIXME: use IPC const notBundledRequire = require; + +// 初期化が完了したかどうかのフラグとPromise +let initialized = false; +let initializationPromise = null; const manager = new ServiceManger(); -const getServiceNameList = () => { - if (process.env.PLAYWRIGHT_TEST === "1" || process.title?.includes("playwright")) { - return notBundledRequire("../../tests/fixtures/test-services.js"); - } else { - const serviceModule = notBundledRequire("../service.js"); - return serviceModule.default || serviceModule; - } -}; -const services = getServiceNameList() - .filter((service) => { - return service.enabled; - }) - .map((service) => { - const { Model, Client } = service.index ? service.index : require(service.indexPath); - const client = new Client(service.options); - return { - model: new Model(), - client: client, - isDefaultChecked: service.isDefaultChecked && client.isLogin() - }; - }); -services.forEach(({ model, client, isDefaultChecked }) => { - manager.addService({ - model, - client, - isDefaultChecked - }); -}); + +// 非同期初期化関数 +async function initializeManager() { + if (initialized) return manager; + if (initializationPromise) return initializationPromise; + + initializationPromise = (async () => { + try { + let serviceList; + if (process.env.PLAYWRIGHT_TEST === "1" || process.title?.includes("playwright")) { + serviceList = notBundledRequire("../../tests/fixtures/test-services.js"); + } else { + const serviceModule = notBundledRequire("../service.js"); + const serviceConfig = serviceModule.default || serviceModule; + + // service.jsがPromiseを返す場合は解決する + if (serviceConfig && typeof serviceConfig.then === "function") { + serviceList = await serviceConfig; + } else { + serviceList = serviceConfig; + } + } + + if (!Array.isArray(serviceList)) { + throw new Error("Service list is not an array"); + } + + const services = serviceList + .filter((service) => { + return service.enabled; + }) + .map((service) => { + const { Model, Client } = service.index ? service.index : require(service.indexPath); + const client = new Client(service.options); + return { + model: new Model(), + client: client, + isDefaultChecked: service.isDefaultChecked && client.isLogin() + }; + }); + + services.forEach(({ model, client, isDefaultChecked }) => { + manager.addService({ + model, + client, + isDefaultChecked + }); + }); + + initialized = true; + return manager; + } catch (error) { + console.error("Failed to initialize services:", error); + throw error; + } + })(); + + return initializationPromise; +} + +// 初期化を待つためのヘルパー関数をエクスポート +export async function waitForInitialization() { + return initializeManager(); +} + +// デフォルトエクスポートはmanagerのままだが、使用前に初期化が必要 export default manager; diff --git a/tests/integration/app.play.js b/tests/integration/app.play.js index 390516b..1712145 100644 --- a/tests/integration/app.play.js +++ b/tests/integration/app.play.js @@ -43,6 +43,13 @@ function createElectronLaunchOptions(additionalArgs = []) { return { args, env, timeout: 30000 }; } +// アプリケーションの初期化完了を待つヘルパー関数 +async function waitForAppInitialization(window) { + // ServiceListが表示されることで初期化完了を確認 + const serviceList = window.locator(".ServiceList"); + await expect(serviceList).toBeVisible({ timeout: 10000 }); +} + // Test contextを使用してElectronアプリケーションとウィンドウを管理 const test = baseTest.extend({ app: async ({}, use) => { @@ -75,10 +82,44 @@ test.describe("Postem Application", () => { // メインコンテナが存在することを確認 const mainDiv = window.locator("#js-main"); await expect(mainDiv).toBeVisible(); + + // ローディング画面が表示される可能性があるので、アプリが完全に初期化されるまで待つ + // ServiceListが表示されることで初期化完了を確認 + const serviceList = window.locator(".ServiceList"); + await expect(serviceList).toBeVisible({ timeout: 10000 }); + }); + + test("ローディング画面が表示され、初期化後に消える", async ({ window }) => { + // ウィンドウが読み込まれるまで待機 + await window.waitForLoadState("domcontentloaded"); + + // ローディング画面の存在を確認(すぐに確認) + const loadingText = window.locator("text=Loading..."); + const isLoadingVisible = await loadingText.isVisible().catch(() => false); + + // ローディング画面が表示されていた場合 + if (isLoadingVisible) { + console.log("Loading screen was displayed"); + + // ServiceListが表示されるまで待つ(初期化完了) + const serviceList = window.locator(".ServiceList"); + await expect(serviceList).toBeVisible({ timeout: 10000 }); + + // ローディング画面が消えたことを確認 + await expect(loadingText).not.toBeVisible(); + } else { + // ローディング画面が表示されなかった場合(高速な初期化) + console.log("Loading screen was not displayed (fast initialization)"); + + // ServiceListがすでに表示されていることを確認 + const serviceList = window.locator(".ServiceList"); + await expect(serviceList).toBeVisible(); + } }); test("エラーハンドリングテスト", async ({ window }) => { await window.waitForLoadState("domcontentloaded"); + await waitForAppInitialization(window); // DEBUGサービスを選択 await window.locator(".ServiceList img").first().click(); @@ -105,6 +146,7 @@ test.describe("Postem Application", () => { test("ウィンドウリサイズ対応テスト", async ({ window }) => { await window.waitForLoadState("domcontentloaded"); + await waitForAppInitialization(window); // 初期サイズでの要素確認 await expect(window.locator(".ServiceList")).toBeVisible(); @@ -150,6 +192,7 @@ test.describe("Postem Application", () => { testWithUrlParams("URLパラメーターからの起動テスト", async ({ windowWithUrlParams }) => { await windowWithUrlParams.waitForLoadState("domcontentloaded"); + await waitForAppInitialization(windowWithUrlParams); // パラメーターが正しく反映されているかチェック // URLフィールドの値を確認 @@ -159,6 +202,7 @@ test.describe("Postem Application", () => { test("統合ワークフロー: Title + URL + Tag選択 + CodeMirror + Cmd+Enter", async ({ window }) => { await window.waitForLoadState("domcontentloaded"); + await waitForAppInitialization(window); // 1. DEBUGサービスを選択 await window.locator(".ServiceList img").first().click(); @@ -225,6 +269,7 @@ test.describe("Postem Application", () => { test("textlint機能の動作確認テスト", async ({ window }) => { await window.waitForLoadState("domcontentloaded"); + await waitForAppInitialization(window); // DEBUGサービスを選択 await window.locator(".ServiceList img").first().click();