Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2017-12-7 #40

Closed
2 of 3 tasks
annewanghy opened this issue Dec 7, 2017 · 9 comments
Closed
2 of 3 tasks

2017-12-7 #40

annewanghy opened this issue Dec 7, 2017 · 9 comments

Comments

@annewanghy
Copy link
Owner

annewanghy commented Dec 7, 2017

  • 3 items -> 4 items
  • TopLevelCategory -> Collection
  • test

frontend/containers/popupShops:

PopupShops.getInitialProps = ({ context }: InitialProps): Promise<ShopProps> =>
  makeService(api.PopUpShopService, context)
    .all({})
    .then(({ popupShops: shops }) => ({ shops }));

frontend/containers/popupShops.test.js
主要测试
Promise对象,resolve和reject。resolve表示成功,reject表示被拒绝

describe("PopupShop.getInitialProps", () => {
  test("happy path", async () => {
    const response: typeof api.PopUpShopService.prototype.all = () =>
      Promise.resolve(new api.PopUpShopList({ popupShops: testProps }));

    api.PopUpShopService.prototype.all = jest.fn(response);

    const props = await PopupShops.getInitialProps(testContext());

    const expected: Props = { shops: testProps };

    expect(props).toMatchObject(expected);
  });

  test("failing API call", () => {
    const error = new Error("test error");

    api.PopUpShopService.prototype.all = jest.fn(() => Promise.reject(error));

    expect.hasAssertions();

    return expect(PopupShops.getInitialProps(testContext())).rejects.toBe(
      error
    );
  });
});

frontend/proto.d.ts, 类型声明文件, PopupShopService里面有一个all方法,返回一个Promise对象

class PopUpShopService extends $protobuf.rpc.Service {

        /**
         * Constructs a new PopUpShopService service.
         * @param rpcImpl RPC implementation
         * @param [requestDelimited=false] Whether requests are length-delimited
         * @param [responseDelimited=false] Whether responses are length-delimited
         */
        constructor(rpcImpl: $protobuf.RPCImpl, requestDelimited?: boolean, responseDelimited?: boolean);

        /**
         * Creates new PopUpShopService service using the specified rpc implementation.
         * @param rpcImpl RPC implementation
         * @param [requestDelimited=false] Whether requests are length-delimited
         * @param [responseDelimited=false] Whether responses are length-delimited
         * @returns RPC service. Useful where requests and/or responses are streamed.
         */
        public static create(rpcImpl: $protobuf.RPCImpl, requestDelimited?: boolean, responseDelimited?: boolean): PopUpShopService;

        /**
         * Calls All.
         * @param request Empty message or plain object
         * @param callback Node-style callback called with the error, if any, and PopUpShopList
         */
        public all(request: google.protobuf.IEmpty, callback: api.PopUpShopService.AllCallback): void;

        /**
         * Calls All.
         * @param request Empty message or plain object
         * @returns Promise
         */
        public all(request: google.protobuf.IEmpty): Promise<api.PopUpShopList>;
    }

frontend/proto.js,实现文件, 用变量PopUpShopService.prototype.all调用all方法

api.PopUpShopService = (function() {
  PopUpShopService.prototype.all = function all(request, callback) {
            return this.rpcCall(all, $root.google.protobuf.Empty, $root.api.PopUpShopList, request, callback);
        };
}

api/popupshop.proto 后端文件有一个service类型的PopUpShopService,有一个rpc类型的All方法,返回一个PopupList

package api;

import "github.com/golang/protobuf/ptypes/empty/empty.proto";
import "github.com/theplant/mastani/popupshop/spec.proto";

message PopUpShopList {
    repeated popupshop.PopUpShop popup_shops = 1;
}

service PopUpShopService {
    rpc All(google.protobuf.Empty) returns (PopUpShopList);
}

错误处理文件在frontend/prottp/index.ts文件下
三个错误,一个networkerror表示网络错误,可以通过Promise的reject得到
而其他两个错误是得到数据之后,验证错误提示哪一个field出错和Authorication权限错误
400以上的都是http错误

type IServiceError =
  | INetworkError
  | IHTTPError
  | IValidationError
  | IAuthenticationError;

const validateStatus = (props: { statusCode: number; body: Uint8Array }) => {
  if (props.statusCode === 422) {
    throw new ValidationError(props.body);
  } else if (props.statusCode === 403) {
    throw new AuthenticationError();
  } else if (props.statusCode >= 400) {
    throw new HTTPError(props.statusCode);
  }
  return props;
};
@annewanghy
Copy link
Owner Author

VSCode 忽略搜索哪些文件夹

"search.exclude": {
    "**/.git": true,
    "**/build": true,
    "**/node_molecules": true,
    "**/bower_components": true,
    "**/tmp": true
  }

@annewanghy
Copy link
Owner Author

use Hugo to make a blog
based on go
quicker than Hexo

@annewanghy
Copy link
Owner Author

annewanghy commented Dec 7, 2017

frontend/containers/product/index.tsx

Product.getInitialProps = ({
  query,
  context
}: InitialProps): Promise<ProductProps> => {
  const productCode: api.IProductCode = { code: (query && query.code) || "" };

  if (!productCode.code) {
    throw errNotFound;
  }

  const service = makeService(api.ProductService, context);

  return service.getProduct(productCode).then(productResponse => {
    if (!productResponse.product) {
      throw errNotFound;
    }

    const properties: base.IProperty[] =
      (productResponse.product && productResponse.product.filterProperties) ||
      [];

    const topLevelCategory: base.IProperty = properties.find(
      ({ field }) => field === "TopLevelCategory"
    ) || { values: [] };

    const filterProperties: products.IFilterProperty[] = [
      {
        field: "TopLevelCategory",
        groupType: products.FilterGroupType.PRODUCT,
        inValues: topLevelCategory.values
      }
    ];

    return service
      .filterProducts({ filterProperties, page: 1, perPage: 5 })
      .then(({ result, rdGroups }) => {
        const products = ((result && result.products) || [])
          .filter(({ code }) => code !== productCode.code)
          .slice(0, 4);

        const productsResponse: api.IProductsResponse = {
          products,
          rdGroups
        };

        return {
          productResponse,
          productsResponse
        } as ProductProps;
      });
  });
};

api/products.proto
api.ProductService
上面用到了GetProduct (ProductCode) returns (ProductResponse)这个方法

service ProductService {
  rpc GetProduct (ProductCode) returns (ProductResponse);
  rpc GetProducts (ProductCodes) returns (ProductsResponse);
  rpc GetFilterGroups (products.SearchOptions) returns (FilterProductsResponse);
  rpc FilterProducts (products.SearchOptions) returns (FilterProductsResponse);
}

没有找到,就throw一个errorNotFound的错误, 输出错误信息product not found

const errNotFound = new ProductNotFound(404);

class ProductNotFound extends Error {
  constructor(public statusCode: number) {
    super("product not found");
  }
}

frontend/proto.d.ts文件里面可以找到base.IProperty

export namespace base {

    /** Properties of a Property. */
    interface IProperty {

        /** Property field */
        field?: string;

        /** Property values */
        values?: string[];
    }
...
}
/** for filter products */
namespace products下的interface IProduct有这个过滤属性,类型就是base.IProperty
filterProperties?: base.IProperty[];  
const properties: base.IProperty[] =
   (productResponse.product && productResponse.product.filterProperties) ||
   [];
而base.IProperty是namespace base下的interface IProperty,这个接口需要两个可选参数filed,和values
const topLevelCategory: base.IProperty = properties.find(
   ({ field }) => field === "TopLevelCategory") || { values: [] };
 设置过滤条件为*TopLevelCategory*, 需要修改为*TopLevelCollection*
 const filterProperties: products.IFilterProperty[] = [
      {
        field: "TopLevelCategory",
        groupType: products.FilterGroupType.PRODUCT,
        inValues: topLevelCategory.values
      }
    ];

class SearchOptions { //选择条件
 class AggregateCondition {//合计条件
  interface IFilterProperty { // 过滤属性
        /** FilterProperty field */
        field?: string;

        /** FilterProperty groupType */
        groupType?: products.FilterGroupType;

        /** FilterProperty inValues */
        inValues?: string[];

        /** FilterProperty isNegative */
        isNegative?: boolean;
    }
 }
}
return service
      .filterProducts({ filterProperties, page: 1, perPage: 5 }) // 用到了page, perPage和filterProperties三个可选参数
      .then(({ result, rdGroups }) => {// 用到了返回函数的result和rdGroups两个参数
        const products = ((result && result.products) || [])  // 得到返回的结果是result.products
          .filter(({ code }) => code !== productCode.code)
          .slice(0, 4);

        const productsResponse: api.IProductsResponse = {
          products,
          rdGroups
        };

        return {
          productResponse,
          productsResponse
        } as ProductProps;
      });
ProductService{
  //返回一个Promise对象
 public filterProducts(request: products.ISearchOptions): Promise<api.FilterProductsResponse>;
}

输入参数
products.ISearchOptions
/** Properties of a SearchOptions. */
    interface ISearchOptions {

        /** SearchOptions keyword */
        keyword?: string;

        /** SearchOptions page */
        page?: (number|Long);

        /** SearchOptions perPage */
        perPage?: (number|Long);

        /** SearchOptions filterProperties */
        filterProperties?: products.IFilterProperty[];

        /** SearchOptions sorters */
        sorters?: products.ISorter[];

        /** SearchOptions conditions */
        conditions?: products.FilterCondition[];

        /** SearchOptions filterGroups */
        filterGroups?: products.ISearchFilterGroup[];

        /** SearchOptions filterGroupMode */
        filterGroupMode?: products.SearchFilterGroupMode;

        /** SearchOptions path */
        path?: string;

        /** SearchOptions publishReady */
        publishReady?: (number|Long);

        /** SearchOptions scheduledAt */
        scheduledAt?: string;
    }

返回参数
class FilterProductsResponse {

        /**
         * Constructs a new FilterProductsResponse.
         * @param [properties] Properties to set
         */
        constructor(properties?: api.IFilterProductsResponse);

        /** FilterProductsResponse result. */
        public result?: (products.IFilterProductsResult|null);

        /** FilterProductsResponse rdGroups. */
        public rdGroups: referencedata.IRDGroup[];
 ...
}

/** Properties of a FilterProductsResult. */
    interface IFilterProductsResult {

        /** FilterProductsResult products */
        products?: products.IProduct[];

        /** FilterProductsResult filterGroups */
        filterGroups?: products.IFilterGroup[];

        /** FilterProductsResult total */
        total?: (number|Long);
    }

@annewanghy
Copy link
Owner Author

annewanghy commented Dec 7, 2017

主要是解决将topLevelCategory换成topLevelCollection的问题

frontend/product/productField.ts

enum ProductField {
  TopLevelCategory = "TopLevelCategory",
  SubCategory = "SubCategory",
  Collection = "Collection",
  Material = "Material",
  ColorDescription = "ColorDescription",
  Size = "Size",
  TotalLength = "TotalLength",
  MaximumWidth = "MaximumWidth"
}
TopLevelCategory
{"categories":{"items":[{"itemType":"TopLevelCategory","code":"TC1","slug":"earrings","name":"ピアス(イヤリング)","description":"多くの文化で身分や美しさの象徴として用いられている。かつては、奴隷身分を示すため、耳から外すことのできないタイプのものが用いられていたところもある。","images":[{"device":"MOBILE","url":"//mastani-dev.s3-ap-southeast-1.amazonaws.com/reference_data_item_images/6/image/key-visual-1.20171109052617018896809.png"},{"device":"LAPTOP","url":"//mastani-dev.s3-ap-southeast-1.amazonaws.com/reference_data_item_images/5/image/key-visual.20171109054317787731532.png","purpose":"earrings category"}]},{"itemType":"TopLevelCategory","code":"TC2","slug":"rings","name":"リング"},{"itemType":"TopLevelCategory","code":"TC3","slug":"necklace","name":"ネックレス"},{"itemType":"TopLevelCategory","code":"TC4","slug":"bracelet","name":"ブレスレット"},{"itemType":"TopLevelCategory","code":"TC5","slug":"brooch","name":"ブローチ"},{"itemType":"TopLevelCategory","code":"TC6","slug":"pendant","name":"ペンダント"},{"itemType":"TopLevelCategory","code":"TC7","slug":"brooch","name":"ブローチ"},{"itemType":"TopLevelCategory","code":"TC8","slug":"accessories","name":"アクセサリー","images":[{"device":"MOBILE","url":"//mastani-dev.s3.ap-southeast-1.amazonaws.com/reference_data_item_images/1/image/anime-like-fate-stay-night.20171102081417498370912.jpg","purpose":"collection page"}]}]},"isOnCartPage":false}

frontend/index.ts

const CollectionList = (limit?: number) => {
  const List = ((props: Props) => (
    <Responsive>
      {(desktop: boolean) =>
        desktop ? <DesktopList {...props} /> : <MobileList {...props} />}
    </Responsive>
  )) as Container<Props>;

  List.getInitialProps = ({ context }: InitialProps): Promise<Props> =>
    makeService(api.ReferenceDataService, context)
      .getItems({ fetchAll: true, itemType: "Collection" })
      .then(({ items }) => {
        items = items.filter(
          ({ images }) => images != null && images.length > 0
        );

        if (limit != null) {
          items = items.slice(0, limit);
        }

        return { items };
      });

  return List;
};

TopLevelCategory

{filterProperties: Array(1), page: 1, perPage: 5}
filterProperties:Array(1)
field:"TopLevelCategory"
groupType:0
inValues:"TC1"

Collection

{filterProperties: Array(1), page: 1, perPage: 5}
filterProperties:Array(1)
field:"Collection"
groupType:0
inValues:["CL4"]

但是最后和Jaden讨论之后,明白产品没有collection这个属性,而是用TopLevelCategory.
那就是不需要修改原来junhui写的内容

@annewanghy
Copy link
Owner Author

annewanghy commented Dec 7, 2017

学习怎么使用postman调用后台数据
导入团队开发的.json文件,然后换到collection的tab页面,发送send请求就可以获取API的数据

本地的API数据 http://localhost:9800/api/ja/api.ProductService/FilterProducts
更换为远程的API数据 https://mastani-api.dt.theplant-dev.com/api/ja/api.ProductService/FilterProducts

image

image

@annewanghy
Copy link
Owner Author

Promise没有完成
product/index.test.tsx
现在的写法用then来调用filter是错误的,因此还需要修改
半成品

import Product from ".";
import { ProductProps } from "./mobile";
import { api, products, base } from "../../proto";
import { productResponse, productsResponse } from "./fakeData";
import { testContext } from "../../testHelper";

const properties: base.IProperty[] =
  (productResponse.product && productResponse.product.filterProperties) ||
  [];

const topLevelCategory: base.IProperty = properties.find(
  ({ field }) => field === "TopLevelCategory"
) || { values: [] };

const filterProperties: products.IFilterProperty[] = [
  {
    field: "TopLevelCategory",
    groupType: products.FilterGroupType.PRODUCT,
    inValues: topLevelCategory.values
  }
];

describe("Product.getInitialProps", () => {
  test("happy filter path", async () => {
    const response: typeof api.ProductService.prototype.getProduct(productResponse.product.code) = () => 
      Promise
        .resolve(new api.ProductResponse(productResponse))
        .then(() => {
            const filterResponse: typeof api.ProductService.prototype.filterProducts({ filterProperties, page: 1, perPage: 5 })= () => 
                Promise.resolve(new api.FilterProductsResponse(productsResponse))
        })

    api.ProductService.prototype.getProduct = jest.fn(response);

    const props = await Product.getInitialProps(testContext());

    const expected: ProductProps = { productResponse: productResponse, productsResponse: productsResponse};

    expect(props).toMatchObject(expected);
  });

  test("failing filter API call", () => {
    const error = new Error("test error");

    api.ProductService.prototype.filterProducts = jest.fn(() =>
      Promise.reject(error)
    );

    expect.hasAssertions();

    return expect(Product.getInitialProps(testContext())).rejects.toBe(error);
  });

  test("failing API call", () => {
    const error = new Error("test error");

    api.ProductService.prototype.getProduct = jest.fn(() =>
      Promise.reject(error)
    );

    expect.hasAssertions();

    return expect(Product.getInitialProps(testContext())).rejects.toBe(error);
  });
});

@annewanghy
Copy link
Owner Author

annewanghy commented Dec 7, 2017

import Product from ".";
import { ProductProps } from "./mobile";
import { api } from "../../proto";
import { productResponse, productsResponse } from "./fakeData";
import { testContext } from "../../testHelper";

describe("Product.getInitialProps", () => {
  test("happy path", async () => {
    const getProductResponse: typeof api.ProductService.prototype.getProduct = () =>
      Promise.resolve(new api.ProductResponse(productResponse));

    const getProductsResponse: typeof api.ProductService.prototype.getFilterGroups = () =>
      Promise.resolve(new api.FilterProductsResponse(productsResponse));

    api.ProductService.prototype.getProduct = jest.fn(getProductResponse);
    api.ProductService.prototype.getFilterGroups = jest.fn(getProductsResponse);

    const props = await Product.getInitialProps({
      ...testContext(),
      query: { code: "xxx" }
    });

    const expected: ProductProps = {
      productResponse: productResponse,
      productsResponse: productsResponse
    };

    expect(props).toMatchObject(expected);
  });

  test("failing API call", () => {
    const error = new Error("test error");

    api.ProductService.prototype.getProduct = jest.fn(() =>
      Promise.reject(error)
    );

    api.ProductService.prototype.getFilterGroups = jest.fn(() =>
      Promise.reject(error)
    );

    expect.hasAssertions();

    return expect(
      Product.getInitialProps({ ...testContext(), query: { code: "xxx" } })
    ).rejects.toBe(error);
  });
});

@annewanghy
Copy link
Owner Author

annewanghy commented Dec 8, 2017

总共12个case

  • error
    • without code - > throw errorNotFound
    • make service error
    • service.getProduct error
    • service.filterProducts error
  • no error
    • filterProperties[0].inValues
      • dosen't has topLevelCategory.values
      • has topLevelCategory.values
    • filterProducts
      • filterProducts dosen't contain product
      • filterProducts contain product
        • filterProducts === null, productsResponse = null
        • filterProducts.length in (1,4) , productsResponse = filterProducts.length
        • filterProducts.length >4 , productsResponse = 4

@annewanghy
Copy link
Owner Author

annewanghy commented Dec 8, 2017

first try

import Product from ".";
import { ProductProps } from "./mobile";
import { api } from "../../proto";
import {
  productResponse,
  filterProductsResponse,
  filteredProductsResponse
} from "./fakeData";
import { testContext } from "../../testHelper";

describe("throw error", () => {
  test("fail to get product code", () => {
    const error = new Error("fail to get product code");

    expect.hasAssertions();

    return expect(
      Product.getInitialProps({
        ...testContext()
      })
    ).rejects.toEqual(error);
  });
  test("fail to make service", () => {
    const error = new RangeError("index out of range: 5 + 4 > 7");

    expect.hasAssertions();

    return expect(
      Product.getInitialProps({
        ...testContext(),
        query: {
          code: "ES-43"
        }
      })
    ).rejects.toEqual(error);
  });

  test("fail to call api getProduct", () => {
    const error = new Error("getProduct error");

    api.ProductService.prototype.getProduct = jest.fn(() =>
      Promise.reject(error)
    );

    expect.hasAssertions();

    return expect(
      Product.getInitialProps({
        ...testContext(),
        query: {
          code: "ES-43"
        }
      })
    ).rejects.toEqual(error);
  });

  test("fail to call api getFilterGroups", () => {
    const error = new Error("getProduct error");

    api.ProductService.prototype.getFilterGroups = jest.fn(() =>
      Promise.reject(error)
    );

    expect.hasAssertions();

    return expect(
      Product.getInitialProps({
        ...testContext(),
        query: {
          code: "ES-43"
        }
      })
    ).rejects.toEqual(error);
  });

  test("failing API call", () => {
    const error = new Error("test error");

    api.ProductService.prototype.getProduct = jest.fn(() =>
      Promise.reject(error)
    );

    api.ProductService.prototype.getFilterGroups = jest.fn(() =>
      Promise.reject(error)
    );

    expect.hasAssertions();

    return expect(
      Product.getInitialProps({
        ...testContext(),
        query: {
          code: "ES-43"
        }
      })
    ).rejects.toEqual(error);
  });
});

describe("Product.getInitialProps", () => {
  test("happy path", async () => {
    const getProductResponse: typeof api.ProductService.prototype.getProduct = () =>
      Promise.resolve(new api.ProductResponse(productResponse));

    const getFilterProductsResponse: typeof api.ProductService.prototype.filterProducts = () =>
      Promise.resolve(new api.FilterProductsResponse(filterProductsResponse));

    api.ProductService.prototype.getProduct = jest.fn(getProductResponse);
    api.ProductService.prototype.filterProducts = jest.fn(
      getFilterProductsResponse
    );

    const props = await Product.getInitialProps({
      ...testContext(),
      query: {
        code: "ES-43"
      }
    });

    const expected: ProductProps = {
      productResponse: productResponse,
      productsResponse: filteredProductsResponse
    };

    expect(props).toMatchObject(expected);
  });
});

describe("test filter data", () => {
  const cases: Array<
    [string, string | null, Array<String> | null, {} | any]
  > = [
    [
      "filtered products don't contain the product",
      "ES-00",
      ["ES-01", "ES-02", "ES-03"],
      ["ES-01", "ES-02", "ES-03"]
    ],
    [
      "filtered products contain the product",
      "ES-00",
      ["ES-00", "ES-01", "ES-02"],
      ["ES-01", "ES-02"]
    ],
    ["filtered products = null", null, null, null],
    [
      "filtered products's length <= 4 and doesn't contain product",
      "ES-00",
      ["ES-01"],
      ["ES-01"]
    ],
    [
      "filtered products'length>4 and doesn't contain the product",
      "ES-00",
      ["ES-01", "ES-02", "ES-03", "ES-04", "ES-05"],
      ["ES-01", "ES-02", "ES-03", "ES-04"]
    ]
  ];

  cases.forEach(([desc, productCode, filterProduct, filteredProduct]) => {
    test(`${desc}`, () => {
      var filterProductResult: Array<String> | null = new Array();
      if (filterProduct && filterProduct.length <= 4) {
        filterProductResult = ((filterProduct && filterProduct) || []).filter(
          product => product !== productCode
        );
      } else {
        filterProductResult = ((filterProduct && filterProduct) || [])
          .filter(product => product !== productCode)
          .slice(0, 4);
      }

      expect(filterProductResult).toMatchObject(filteredProduct);
    });
  });
});

两个错误“Expected at least one assertion to be called but received none.”
一个不匹配

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant